/* Copyright (C) 2004 - 2008  Versant Inc.  http://www.db4o.com

This file is part of the sharpen open source java to c# translator.

sharpen is free software; you can redistribute it and/or modify it under
the terms of version 2 of the GNU General Public License as published
by the Free Software Foundation and as clarified by db4objects' GPL 
interpretation policy, available at
http://www.db4o.com/about/company/legalpolicies/gplinterpretation/
Alternatively you can write to db4objects, Inc., 1900 S Norfolk Street,
Suite 350, San Mateo, CA 94403, USA.

sharpen is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
for more details.

You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
59 Temple Place - Suite 330, Boston, MA  02111-1307, USA. */

package sharpen.core;


import org.eclipse.jdt.core.IBuffer;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.jdt.core.dom.*;
import sharpen.core.Configuration.MemberMapping;
import sharpen.core.csharp.ast.*;
import sharpen.core.framework.*;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static sharpen.core.framework.Environments.my;
import static sharpen.core.framework.StaticImports.isStaticImport;
import static sharpen.core.framework.StaticImports.staticImportMethodBinding;

public class CSharpBuilder extends ASTVisitor {

    private static final String JAVA_LANG_VOID_TYPE = "java.lang.Void.TYPE";
    private static final String JAVA_LANG_BOOLEAN_TYPE = "java.lang.Boolean.TYPE";
    private static final String JAVA_LANG_CHARACTER_TYPE = "java.lang.Character.TYPE";
    private static final String JAVA_LANG_INTEGER_TYPE = "java.lang.Integer.TYPE";
    private static final String JAVA_LANG_LONG_TYPE = "java.lang.Long.TYPE";
    private static final String JAVA_LANG_BYTE_TYPE = "java.lang.Byte.TYPE";
    private static final String JAVA_LANG_SHORT_TYPE = "java.lang.Short.TYPE";
    private static final String JAVA_LANG_FLOAT_TYPE = "java.lang.Float.TYPE";
    private static final String JAVA_LANG_DOUBLE_TYPE = "java.lang.Double.TYPE";

    private static final Pattern SUMMARY_CLOSURE_PATTERN = Pattern.compile("\\.(\\s|$)");
    private static final Pattern HTML_ANCHOR_PATTERN = Pattern.compile("<([aA])\\s+.+>");

    private static final CSTypeReference OBJECT_TYPE_REFERENCE = new CSTypeReference("object");

    private final CSCompilationUnit _compilationUnit;

    protected CSTypeDeclaration _currentType;

    protected CSTypeDeclaration _currentAuxillaryType;

    private String _content;

    private CSBlock _currentBlock;

    private CSExpression _currentExpression;

    protected CSMethodBase _currentMethod;

    protected BodyDeclaration _currentBodyDeclaration;

    private CSLabelStatement _currentContinueLabel;

    protected CompilationUnit _ast;

    protected Configuration _configuration;

    private ASTResolver _resolver;

    private IVariableBinding _currentExceptionVariable;

    private final DynamicVariable<Boolean> _ignoreExtends = new DynamicVariable<Boolean>(Boolean.FALSE);

    private List<Initializer> _instanceInitializers = new ArrayList<Initializer>();

    private Stack<Set<String>> _blockVariables = new Stack<Set<String>>();
    private Stack<Set<String>> _localBlockVariables = new Stack<Set<String>>();
    private Stack<HashMap<String, String>> _renamedVariables = new Stack<HashMap<String, String>>();

    private ITypeBinding _currentExpectedType;

    private final Set<String> nullablePrimitivesVariables = new HashSet<>();

    protected NamingStrategy namingStrategy() {
        return _configuration.getNamingStrategy();
    }

    protected WarningHandler warningHandler() {
        return _configuration.getWarningHandler();
    }

    public CSharpBuilder() {
        _configuration = my(Configuration.class);
        _ast = my(CompilationUnit.class);
        _resolver = my(ASTResolver.class);
        _compilationUnit = my(CSCompilationUnit.class);
        if (!_configuration.sharpenNamespace().equals("nonamespace")) {
            _compilationUnit.addUsing(new CSUsing(_configuration.sharpenNamespace()));
        }
    }

    protected CSharpBuilder(CSharpBuilder other) {
        _configuration = other._configuration;
        _ast = other._ast;
        _resolver = other._resolver;
        _compilationUnit = other._compilationUnit;

        _currentType = other._currentType;
        _currentBlock = other._currentBlock;
        _currentExpression = other._currentExpression;
        _currentMethod = other._currentMethod;
        _currentBodyDeclaration = other._currentBodyDeclaration;
    }

    public void setSourceCompilationUnit(CompilationUnit ast) {
        _ast = ast;
    }

    public void setSourceContent(String content) {
        _content = content;
    }

    public void run() {
        if (null == warningHandler() || null == _ast) {
            throw new IllegalStateException();
        }
        _ast.accept(this);
        visit(_ast.getCommentList());
    }

    @Override
    public boolean visit(LineComment node) {
        _compilationUnit.addComment(new CSLineComment(node.getStartPosition(), getText(node.getStartPosition(), node.getLength())));
        return false;
    }

    @Override
    public boolean visit(BlockComment node) {
        _compilationUnit.addComment(new CSBlockComment(node.getStartPosition(), getText(node.getStartPosition(), node.getLength())));
        return false;
    }

    private String getText(int startPosition, int length) {
        try {
            ICompilationUnit cu = (ICompilationUnit) _ast.getJavaElement();
            if (cu != null) {
                IBuffer buffer = cu.getBuffer();
                if (buffer != null) {
                    return buffer.getText(startPosition, length);
                }
            }

            if (_content != null && !_content.isEmpty()) {
                return _content.substring(startPosition, startPosition + length);
            }

            return "";
        } catch (JavaModelException e) {
            throw new RuntimeException(e);
        }
    }

    public CSCompilationUnit compilationUnit() {
        return _compilationUnit;
    }

    public boolean visit(ImportDeclaration node) {
        return false;
    }

    public boolean visit(final EnumDeclaration node) {
        if (processIgnoredType(node)) {
            return false;
        }

        try {
            my(NameScope.class).enterTypeDeclaration(node);
            _ignoreExtends.using(ignoreExtends(node), new Runnable() {
                public void run() {
                    final ITypeBinding binding = node.resolveBinding();
                    if (!binding.isNested()) {
                        processTypeDeclaration(node);
                        return;
                    }

                    if (isNonStaticNestedType(binding)) {
                        processNonStaticNestedTypeDeclaration(node);
                        return;
                    }

                    new CSharpBuilder(CSharpBuilder.this).processTypeDeclaration(node);
                }
            });
        } finally {
            my(NameScope.class).leaveTypeDeclaration(node);
        }

        return false;
    }

    @Override
    public boolean visit(AnnotationTypeDeclaration node) {
        if (processIgnoredType(node)) {
            return false;
        }

        try {
            my(NameScope.class).enterTypeDeclaration(node);
            _ignoreExtends.using(ignoreExtends(node), new Runnable() {
                public void run() {
                    new CSharpBuilder(CSharpBuilder.this).processTypeDeclaration(node);
                }
            });
        } finally {
            my(NameScope.class).leaveTypeDeclaration(node);
        }
        return false;
    }

    @Override
    public boolean visit(MarkerAnnotation node) {
        // TODO: SHA-51
        return false;
    }

    @Override
    public boolean visit(NormalAnnotation node) {
        // TODO: SHA-51
        return false;
    }

    public boolean visit(final LabeledStatement node) {
        String identifier = node.getLabel().getIdentifier();
        _currentContinueLabel = new CSLabelStatement(continueLabel(identifier));
        try {
            node.getBody().accept(this);
        } finally {
            _currentContinueLabel = null;
        }
        addStatement(new CSLabelStatement(breakLabel(identifier)));
        return false;
    }

    private String breakLabel(String identifier) {
        return identifier + "_break";
    }

    private String continueLabel(String identifier) {
        return identifier + "_continue";
    }

    public boolean visit(SuperFieldAccess node) {
        String name = mappedFieldName(node);
        if (null == node.getQualifier()) {
            pushExpression(new CSMemberReferenceExpression(new CSBaseExpression(), name));
        } else {
            notImplemented(node);
        }
        return false;
    }

    public boolean visit(MemberRef node) {
        notImplemented(node);
        return false;
    }

    @Override
    public boolean visit(WildcardType node) {
        notImplemented(node);
        return false;
    }

    private void notImplemented(ASTNode node) {
        throw new IllegalArgumentException(sourceInformation(node) + ": " + node.toString());
    }

    public boolean visit(PackageDeclaration node) {
        _compilationUnit.setPackagePosition(node.getStartPosition());
        String namespace = node.getName().toString();
        _compilationUnit.namespace(mappedNamespace(namespace));

        processDisableTags(node, _compilationUnit);
        return false;
    }

    public boolean visit(AnonymousClassDeclaration node) {
        CSAnonymousClassBuilder builder = mapAnonymousClass(node);
        pushExpression(builder.createConstructorInvocation());
        return false;
    }

    private CSAnonymousClassBuilder mapAnonymousClass(AnonymousClassDeclaration node) {
        CSAnonymousClassBuilder builder = new CSAnonymousClassBuilder(this, node);
        _currentType.addMember(builder.type());
        return builder;
    }

    private CSTypeDeclaration staticType(TypeDeclaration node) {
        CSTypeDeclaration type = null;
        if (node.getParent() instanceof CompilationUnit) {
            CSTypeDeclaration found = null;
            for (CSType t : _compilationUnit.types()) {
                if (t instanceof CSTypeDeclaration && t.name().equals(node.getName().toString()) && ((CSTypeDeclaration) t).typeParameters().isEmpty()) {
                    found = (CSTypeDeclaration) t;
                    break;
                }
            }
            if (found != null) {
                type = found;
            } else {
                type = new CSClass(node.getName().toString(), CSClassModifier.Static);
                type.visibility(CSVisibility.Public);
                _compilationUnit.addType(type);
            }
        } else if (node.getParent() instanceof TypeDeclaration) {
            CSTypeDeclaration parent = staticType((TypeDeclaration) node.getParent());
            CSTypeDeclaration found = null;
            for (CSMember member : parent.members()) {
                if (member instanceof CSTypeDeclaration && member.name().equals(node.getName().toString()) && ((CSTypeDeclaration) member).typeParameters().isEmpty()) {
                    found = (CSTypeDeclaration) member;
                    break;
                }
            }
            if (found != null) {
                type = found;
            } else {
                type = new CSClass(node.getName().toString(), CSClassModifier.Static);
                type.visibility(CSVisibility.Public);
                parent.addMember(type);
            }
        } else {
            unsupportedConstruct(node, "Unexpected parrent:" + node.getParent());
        }
        return type;
    }

    public boolean visit(final TypeDeclaration node) {

        if (processIgnoredType(node)) {
            return false;
        }

        try {
            my(NameScope.class).enterTypeDeclaration(node);

            _ignoreExtends.using(ignoreExtends(node), new Runnable() {
                public void run() {

                    final ITypeBinding binding = node.resolveBinding();
                    if (!binding.isNested()) {
                        processTypeDeclaration(node);
                        return;
                    }

                    if (isNonStaticNestedType(binding)) {
                        processNonStaticNestedTypeDeclaration(node);
                        return;
                    }
                    if (binding.isNested() && isStatic(binding) && hasStaticHelpersAnnotation(node.getParent())) {
                        CSharpBuilder builder = new CSharpBuilder(CSharpBuilder.this);
                        builder._currentType = staticType((TypeDeclaration) node.getParent());
                        builder.processTypeDeclaration(node);
                    } else {
                        new CSharpBuilder(CSharpBuilder.this).processTypeDeclaration(node);
                    }
                }
            });
        } finally {
            my(NameScope.class).leaveTypeDeclaration(node);
        }

        return false;
    }

    protected boolean isPrivate(MethodDeclaration node) {
        return Modifier.isPrivate(node.getModifiers());
    }

    private boolean processIgnoredType(AbstractTypeDeclaration node) {
        if (!SharpenAnnotations.hasIgnoreAnnotation(node) && !hasRemoveAnnotation(node) && !_configuration.isIgnoredType(qualifiedName(node.resolveBinding()))) {
            return false;
        }
        if (isMainType(node)) {
            compilationUnit().ignore(true);
        }
        return true;
    }

    private void processNonStaticNestedTypeDeclaration(AbstractTypeDeclaration node) {
        if (!(node instanceof TypeDeclaration)) {
            unsupportedConstruct(node, "Declarations of type " + node.getClass().getName() + " cannot be non-static nested classes.");
        }

        new NonStaticNestedClassBuilder(this, (TypeDeclaration) node);
    }

    protected CSTypeDeclaration processTypeDeclaration(AbstractTypeDeclaration node) {
        CSTypeDeclaration type = mapTypeDeclaration(node);
        mapAnnotations(node, type);
        CSTypeDeclaration auxillaryType = mapAuxillaryTypeDeclaration(node);

        processDisabledType(node, isMainType(node) ? _compilationUnit : type);

        if (_configuration.shouldMakePartial(node.getName().getFullyQualifiedName()))
            type.partial(true);

        ITypeBinding typeBinding = node.resolveBinding();
        addType(typeBinding, type);
        if (auxillaryType != null) {
            addType(typeBinding, auxillaryType);
        }

        mapSuperTypes(node, type);

        mapVisibility(node, type);
        adjustVisibility(typeBinding.getSuperclass(), type);
        if (auxillaryType != null) {
            mapVisibility(node, auxillaryType);
            adjustVisibility(typeBinding.getSuperclass(), auxillaryType);
        }

        mapDocumentation(node, type);
        processConversionJavadocTags(node, type);

        mapMembers(node, type, auxillaryType);

        if (node instanceof EnumDeclaration) {
            if (!ASTUtility.hasConstructorMethod(node)) {
                implementDefaultEnumConstructor(type);
            }

            implementEnumValuesMethod((EnumDeclaration) node, type);
            implementEnumRegisterValuesStaticConstructor((EnumDeclaration) node, type);
        }

        autoImplementCloneable(node, type);

        moveInitializersDependingOnThisReferenceToConstructor(type);

        if (_configuration.junitConversion() && hasTests(type))
            type.addAttribute(new CSAttribute("NUnit.Framework.TestFixture"));

        return type;
    }

    private void processDisabledType(AbstractTypeDeclaration node, CSNode type) {
        final String expression = _configuration.conditionalCompilationExpressionFor(packageNameFor(node));
        if (null != expression) {
            compilationUnit().addEnclosingIfDef(expression);
        }

        processDisableTags(node, type);
    }

    private String packageNameFor(AbstractTypeDeclaration node) {
        ITypeBinding type = node.resolveBinding();
        return type.getPackage().getName();
    }

    protected void flushInstanceInitializers(CSTypeDeclaration type, int startStatementIndex) {

        if (_instanceInitializers.isEmpty()) {
            return;
        }

        ensureConstructorsFor(type);

        int initializerIndex = startStatementIndex;
        for (Initializer node : _instanceInitializers) {
            final CSBlock body = mapInitializer(node);

            for (CSConstructor ctor : type.constructors()) {
                if (ctor.isStatic() || hasChainedThisInvocation(ctor)) {
                    continue;
                }
                ctor.body().addStatement(initializerIndex, body);
            }

            ++initializerIndex;
        }

        _instanceInitializers.clear();
    }

    private CSBlock mapInitializer(Initializer node) {
        final CSConstructor template = new CSConstructor();
        visitBodyDeclarationBlock(node, node.getBody(), template);
        return template.body();
    }

    private boolean hasChainedThisInvocation(CSConstructor ctor) {
        final CSConstructorInvocationExpression chained = ctor.chainedConstructorInvocation();
        return chained != null && chained.expression() instanceof CSThisExpression;
    }

    private void moveInitializersDependingOnThisReferenceToConstructor(CSTypeDeclaration type) {

        final HashSet<String> memberNames = memberNameSetFor(type);

        int index = 0;
        for (CSMember member : copy(type.members())) {
            if (!(member instanceof CSField))
                continue;

            final CSField field = (CSField) member;
            if (!isDependentOnThisOrMember(field, memberNames))
                continue;

            moveFieldInitializerToConstructors(field, type, index++);
        }
    }

    private HashSet<String> memberNameSetFor(CSTypeDeclaration type) {
        final HashSet<String> members = new HashSet<String>();
        for (CSMember member : type.members()) {
            if (member instanceof CSType)
                continue;
            if (isStatic(member))
                continue;
            members.add(member.name());
        }
        return members;
    }

    private boolean isStatic(CSMember member) {
        if (member instanceof CSField)
            return isStatic((CSField) member);
        if (member instanceof CSMethod)
            return isStatic((CSMethod) member);
        return false;
    }

    private boolean isStatic(CSMethod method) {
        return method.modifier() == CSMethodModifier.Static;
    }

    private boolean isStatic(CSField member) {
        final Set<CSFieldModifier> fieldModifiers = member.modifiers();
        return fieldModifiers.contains(CSFieldModifier.Static)
                || fieldModifiers.contains(CSFieldModifier.Const);
    }

    private CSMember[] copy(final List<CSMember> list) {
        return list.toArray(new CSMember[0]);
    }

    private boolean isDependentOnThisOrMember(CSField field, final Set<String> fields) {
        if (null == field.initializer())
            return false;

        final ByRef<Boolean> foundThisReference = new ByRef<Boolean>(false);
        field.initializer().accept(new CSExpressionVisitor() {
            @Override
            public void visit(CSThisExpression node) {
                foundThisReference.value = true;
            }

            @Override
            public void visit(CSReferenceExpression node) {
                if (fields.contains(node.name())) {
                    foundThisReference.value = true;
                }
            }
        });
        return foundThisReference.value;
    }

    private void moveFieldInitializerToConstructors(CSField field, CSTypeDeclaration type, int index) {
        final CSExpression initializer = field.initializer();
        for (CSConstructor ctor : ensureConstructorsFor(type))
            ctor.body().addStatement(
                    index,
                    newAssignment(field, initializer));
        field.initializer(null);
    }

    private CSExpression newAssignment(CSField field, final CSExpression initializer) {
        return CSharpCode.newAssignment(CSharpCode.newReference(field), initializer);
    }

    private Iterable<CSConstructor> ensureConstructorsFor(CSTypeDeclaration type) {
        final List<CSConstructor> ctors = type.constructors();
        if (!ctors.isEmpty())
            return ctors;

        return Collections.singletonList(addDefaultConstructor(type));
    }

    private CSConstructor addDefaultConstructor(CSTypeDeclaration type) {
        final CSConstructor ctor = CSharpCode.newPublicConstructor();
        type.addMember(ctor);
        return ctor;
    }

    private void implementDefaultEnumConstructor(CSTypeDeclaration type) {
        CSConstructor constructor = new CSConstructor(CSVisibility.Private);
        addEnumBaseConstructorParameters(constructor);
        implementEnumBaseConstructorInvocation(constructor);
        type.addMember(constructor);
    }

    private boolean isEnumConstructor(MethodDeclaration node) {
        return node.isConstructor() && node.getParent() instanceof EnumDeclaration;
    }

    private void addEnumBaseConstructorParameters(CSParameterized constructor) {
        constructor.addParameter(new CSVariableDeclaration("ordinal", new CSTypeReference("int")));
        constructor.addParameter(new CSVariableDeclaration("name", new CSTypeReference("string")));
    }

    private void implementEnumBaseConstructorInvocation(CSConstructor constructor) {
        CSConstructorInvocationExpression cie = new CSConstructorInvocationExpression(new CSBaseExpression());
        cie.addArgument(new CSReferenceExpression("ordinal"));
        cie.addArgument(new CSReferenceExpression("name"));
        constructor.chainedConstructorInvocation(cie);
    }

    private void implementEnumValuesMethod(EnumDeclaration enumNode, CSTypeDeclaration type) {
        CSTypeReference enumType = new CSTypeReference(enumNode.getName().getIdentifier());

        CSMethod method = new CSMethod(methodName("values"));
        method.visibility(CSVisibility.Public);
        method.modifier(CSMethodModifier.Static);
        method.returnType(new CSArrayTypeReference(enumType, 1));

        CSArrayInitializerExpression arrayInitializerExpression = new CSArrayInitializerExpression();
        for (Object o : enumNode.enumConstants()) {
            EnumConstantDeclaration constantDeclaration = (EnumConstantDeclaration) o;
            String constantIdentifier = fieldName(constantDeclaration);
            arrayInitializerExpression.addExpression(new CSReferenceExpression(constantIdentifier));
        }

        CSArrayCreationExpression arrayCreationExpression = new CSArrayCreationExpression(enumType);
        arrayCreationExpression.initializer(arrayInitializerExpression);
        method.body().addStatement(new CSReturnStatement(-1, arrayCreationExpression));

        type.addMember(method);
    }

    private void implementEnumRegisterValuesStaticConstructor(EnumDeclaration enumNode, CSTypeDeclaration type) {
        CSConstructor staticConstructor = new CSConstructor(CSConstructorModifier.Static);
        CSMethodInvocationExpression methodInvocationExpression = new CSMethodInvocationExpression(new CSReferenceExpression("RegisterValues"));
        methodInvocationExpression.addTypeArgument(new CSTypeReference(enumNode.getName().getIdentifier()));
        methodInvocationExpression.addArgument(new CSMethodInvocationExpression(new CSReferenceExpression(methodName("values"))));
        staticConstructor.body().addStatement(methodInvocationExpression);
        type.addMember(staticConstructor);
    }

    private void autoImplementCloneable(AbstractTypeDeclaration node, CSTypeDeclaration type) {

        if (!implementsCloneable(type)) {
            return;
        }

        CSMethod clone = new CSMethod("System.ICloneable.Clone");
        clone.returnType(OBJECT_TYPE_REFERENCE);
        clone.body().addStatement(
                new CSReturnStatement(-1,
                        new CSMethodInvocationExpression(new CSReferenceExpression("MemberwiseClone"))));

        type.addMember(clone);
    }

    private boolean implementsCloneable(CSTypeDeclaration node) {
        for (CSTypeReferenceExpression typeRef : node.baseTypes()) {
            if (typeRef.toString().equals("System.ICloneable")) {
                return true;
            }
        }
        return false;
    }

    private void mapSuperTypes(AbstractTypeDeclaration node, CSTypeDeclaration type) {
        if (!_ignoreExtends.value()) {
            mapSuperClass(node, type);
        }
        if (!ignoreImplements(node)) {
            mapSuperInterfaces(node, type);
        }
    }

    private boolean ignoreImplements(AbstractTypeDeclaration node) {
        return containsJavadoc(node, SharpenAnnotations.SHARPEN_IGNORE_IMPLEMENTS);
    }

    private boolean ignoreExtends(AbstractTypeDeclaration node) {
        return containsJavadoc(node, SharpenAnnotations.SHARPEN_IGNORE_EXTENDS);
    }

    private void processConversionJavadocTags(AbstractTypeDeclaration node, CSTypeDeclaration type) {
        processPartialTagElement(node, type);
    }

    private CSTypeDeclaration mapTypeDeclaration(AbstractTypeDeclaration node) {
        if (!(node instanceof TypeDeclaration) && !(node instanceof EnumDeclaration) && !(node instanceof AnnotationTypeDeclaration)) {
            unsupportedConstruct(node, "Cannot map type declaration for node.");
        }

        CSTypeDeclaration type = typeDeclarationFor(node);
        type.startPosition(node.getStartPosition());
        type.sourceLength(node.getLength());
        if (node instanceof TypeDeclaration) {
            mapTypeParameters(((TypeDeclaration) node).typeParameters(), type);
        }
        return checkForMainType(node, type);
    }

    private CSTypeDeclaration mapAuxillaryTypeDeclaration(AbstractTypeDeclaration node) {
        if (!(node instanceof TypeDeclaration) && !(node instanceof EnumDeclaration) && !(node instanceof AnnotationTypeDeclaration)) {
            unsupportedConstruct(node, "Cannot map auxillary type declaration for node.");
        }

        CSTypeDeclaration type = auxillaryTypeDeclarationFor(node);
        if (type == null) {
            return null;
        }

        type.startPosition(node.getStartPosition());
        type.sourceLength(node.getLength());
        return type;
    }

    private void mapTypeParameters(final List typeParameters, CSTypeParameterProvider type) {
        for (Object item : typeParameters) {
            type.addTypeParameter(mapTypeParameter((TypeParameter) item));
        }
    }

    private CSTypeParameter mapTypeParameter(TypeParameter item) {
        CSTypeParameter tp = new CSTypeParameter(identifier(item.getName()));
        ITypeBinding tb = item.resolveBinding();
        if (tb != null) {
            for (ITypeBinding binding : mapTypeParameterExtendedType(tb)) {
                tp.superClass(mappedTypeReference(binding));
            }
        }
        return tp;
    }

    private CSTypeDeclaration typeDeclarationFor(AbstractTypeDeclaration node) {
        return typeDeclarationFor(node, false);
    }

    private CSTypeDeclaration auxillaryTypeDeclarationFor(AbstractTypeDeclaration node) {
        return typeDeclarationFor(node, true);
    }

    private CSTypeDeclaration typeDeclarationFor(AbstractTypeDeclaration node, boolean auxillary) {
        final String typeName = typeName(node);
        if (node instanceof TypeDeclaration && ((TypeDeclaration) node).isInterface()) {
            boolean valid = isValidCSInterface(node.resolveBinding());
            if (valid && auxillary) {
                return null;
            } else if (valid || (!auxillary && _configuration.separateInterfaceConstants())) {
                boolean overriddenName = !typeName.equals(node.getName().toString());
                if (overriddenName) {
                    return new CSInterface(typeName);
                } else {
                    return new CSInterface(processInterfaceName(node));
                }
            } else if (auxillary) {
                return new CSClass(processInterfaceConstantsName(node), CSClassModifier.Static);
            } else {
                warningHandler().warning(node, "Java interface converted to C# class.");
                return new CSClass(typeName, CSClassModifier.Abstract);
            }
        }

        if (auxillary) {
            return null;
        }

        if (isStruct(node)) {
            return new CSStruct(typeName);
        }

        CSClassModifier modifier;
        if (node instanceof TypeDeclaration || node instanceof AnnotationTypeDeclaration) {
            modifier = mapClassModifier(node.getModifiers());
        } else if (node instanceof EnumDeclaration) {
            modifier = CSClassModifier.Sealed;
        } else {
            unsupportedConstruct(node, "cannot determine modifiers for type node");
            throw new UnsupportedOperationException("shouldn't be reachable");
        }

        return new CSClass(typeName, modifier);
    }

    private String typeName(AbstractTypeDeclaration node) {
        String renamed = annotatedRenaming(node);
        if (renamed != null)
            return renamed;
        renamed = mappedTypeName(node.resolveBinding());
        if (renamed != null) {
            int i = renamed.lastIndexOf('.');
            if (i != -1)
                return renamed.substring(i + 1);
            else
                return renamed;
        }
        return node.getName().toString();
    }

    private boolean isStruct(AbstractTypeDeclaration node) {
        return containsJavadoc(node, SharpenAnnotations.SHARPEN_STRUCT);
    }

    private CSTypeDeclaration checkForMainType(AbstractTypeDeclaration node, CSTypeDeclaration type) {
        if (isMainType(node)) {
            String name = type.name();
            if (_configuration.paramCountFileNames()) {
                if (type.typeParameters().size() > 0) {
                    name += "`" + Integer.toString(type.typeParameters().size());
                }
            }

            setCompilationUnitElementName(name);
        }
        return type;
    }

    private void setCompilationUnitElementName(String name) {
        _compilationUnit.elementName(name + ".cs");
    }

    private String processInterfaceName(AbstractTypeDeclaration node) {

        String name = node.getName().getFullyQualifiedName();
        return interfaceName(name);
    }

    private String processInterfaceConstantsName(AbstractTypeDeclaration node) {
        String name = node.getName().getFullyQualifiedName();
        return name + "Constants";
    }

    private boolean isMainType(AbstractTypeDeclaration node) {
        return node.isPackageMemberTypeDeclaration() && Modifier.isPublic(node.getModifiers());
    }

    private void mapSuperClass(AbstractTypeDeclaration node, CSTypeDeclaration type) {
        if (handledExtends(node, type))
            return;

        if (node instanceof EnumDeclaration) {
            type.addBaseType(new CSTypeReference(_configuration.baseEnumType()));
            return;
        }

        if (node instanceof AnnotationTypeDeclaration) {
            type.addBaseType(new CSTypeReference("System.Attribute"));
            return;
        }

        if (!(node instanceof TypeDeclaration)) {
            return;
        }

        Type superclassType = ((TypeDeclaration) node).getSuperclassType();
        if (superclassType == null) {
            return;
        }

        final ITypeBinding superClassBinding = superclassType.resolveBinding();
        if (null == superClassBinding)
            unresolvedTypeBinding(superclassType);

        if (!isLegacyTestFixtureClass(superClassBinding))
            type.addBaseType(mappedTypeReference(superClassBinding));
        else {
            type.addAttribute(new CSAttribute("NUnit.Framework.TestFixture"));
        }
    }

    private boolean isLegacyTestFixtureClass(ITypeBinding type) {
        return (_configuration.junitConversion() && type.getQualifiedName().equals("junit.framework.TestCase"));
    }

    private boolean isLegacyTestFixture(ITypeBinding type) {
        if (!_configuration.junitConversion())
            return false;
        if (isLegacyTestFixtureClass(type))
            return true;
        ITypeBinding base = type.getSuperclass();
        return (base != null) && isLegacyTestFixture(base);
    }

    private boolean hasTests(CSTypeDeclaration type) {
        for (CSMember m : type.members()) {
            if (m instanceof CSMethod) {
                CSMethod met = (CSMethod) m;
                for (CSAttribute at : met.attributes()) {
                    if (at.name().equals("Test") || at.name().equals("NUnit.Framework.Test"))
                        return true;
                }
            }
        }
        return false;
    }

    private boolean handledExtends(AbstractTypeDeclaration node, CSTypeDeclaration type) {
        if (!(node instanceof TypeDeclaration) && !(node instanceof EnumDeclaration) && !(node instanceof AnnotationTypeDeclaration)) {
            unsupportedConstruct(node, "Cannot handle extends for type node.");
        }

        final TagElement replaceExtendsTag = javadocTagFor(node, SharpenAnnotations.SHARPEN_EXTENDS);
        if (null == replaceExtendsTag)
            return false;

        final String baseType = JavadocUtility.singleTextFragmentFrom(replaceExtendsTag);
        type.addBaseType(new CSTypeReference(baseType));
        return true;
    }

    private void mapSuperInterfaces(AbstractTypeDeclaration node, CSTypeDeclaration type) {
        List superInterfaceTypes;
        if (node instanceof TypeDeclaration) {
            superInterfaceTypes = ((TypeDeclaration) node).superInterfaceTypes();
        } else if (node instanceof EnumDeclaration) {
            superInterfaceTypes = ((EnumDeclaration) node).superInterfaceTypes();
        } else if (node instanceof AnnotationTypeDeclaration) {
            superInterfaceTypes = Collections.emptyList();
        } else {
            unsupportedConstruct(node, "cannot map super interfaces for type node");
            throw new UnsupportedOperationException("shouldn't be reachable");
        }

        final ITypeBinding serializable = resolveWellKnownType("java.io.Serializable");
        for (Object itf : superInterfaceTypes) {
            Type iface = (Type) itf;
            if (iface.resolveBinding() == serializable) {
                continue;
            }
            type.addBaseType(mappedTypeReference(iface, false));
        }

        if (!type.isInterface() && node.resolveBinding().isSubTypeCompatible(serializable)) {
            type.addAttribute(new CSAttribute("System.Serializable"));
        }
    }

    private ITypeBinding resolveWellKnownType(String typeName) {
        return _ast.getAST().resolveWellKnownType(typeName);
    }

    private void mapMembers(AbstractTypeDeclaration node, CSTypeDeclaration type, CSTypeDeclaration auxillaryType) {
        if (!(node instanceof TypeDeclaration) && !(node instanceof EnumDeclaration) && !(node instanceof AnnotationTypeDeclaration)) {
            unsupportedConstruct(node, "Cannot map members for node.");
        }

        CSTypeDeclaration saved = _currentType;
        CSTypeDeclaration savedAuxillary = _currentAuxillaryType;
        _currentType = type;
        _currentAuxillaryType = auxillaryType;
        try {
            if (node instanceof EnumDeclaration) {
                visit(((EnumDeclaration) node).enumConstants());
            }
            visit(node.bodyDeclarations());
            createInheritedAbstractMemberStubs(node);
            flushInstanceInitializers(type, 0);
        } finally {
            _currentType = saved;
            _currentAuxillaryType = savedAuxillary;
        }
    }

    private void mapVisibility(BodyDeclaration node, CSMember member) {
        member.visibility(mapVisibility(node));
    }

    protected boolean isNonStaticNestedType(ITypeBinding binding) {
        if (binding.isInterface())
            return false;
        if (!binding.isNested())
            return false;
        return !isStatic(binding);
    }

    private boolean isStatic(ITypeBinding binding) {
        return Modifier.isStatic(binding.getModifiers());
    }

    private void addType(ITypeBinding binding, CSType type) {
        if (null != _currentType && !isExtractedNestedType(binding)) {
            _currentType.addMember(type);
        } else {
            _compilationUnit.addType(type);
        }
    }

    private void mapDocumentation(final BodyDeclaration bodyDecl, final CSMember member) {
        my(PreserveFullyQualifiedNamesState.class).using(true, new Runnable() {
            public void run() {
                if (processDocumentationOverlay(member)) {
                    return;
                }

                mapJavadoc(bodyDecl, member);
                mapDeclaredExceptions(bodyDecl, member);

            }
        });
    }

    private void mapDeclaredExceptions(BodyDeclaration bodyDecl, CSMember member) {
        if (!(bodyDecl instanceof MethodDeclaration))
            return;

        MethodDeclaration method = (MethodDeclaration) bodyDecl;
        mapThrownExceptions(method.thrownExceptionTypes(), member);
    }

    private void mapThrownExceptions(List thrownExceptions, CSMember member) {
        for (Object exception : thrownExceptions) {
            mapThrownException(((SimpleType) exception).getName(), member);
        }
    }

    private void mapThrownException(Name exception, CSMember member) {
        ITypeBinding type = exception.resolveTypeBinding();
        if (SharpenAnnotations.hasIgnoreAnnotation(findDeclaringNode(type))) {
            return;
        }

        final String typeName = mappedTypeName(type);
        if (containsExceptionTagWithCRef(member, typeName))
            return;

        member.addDoc(newTagWithCRef("exception", typeName));
    }

    private boolean containsExceptionTagWithCRef(CSMember member, String cref) {
        for (CSDocNode node : member.docs()) {
            if (!(node instanceof CSDocTagNode))
                continue;

            if (cref.equals(((CSDocTagNode) node).getAttribute("cref"))) {
                return true;
            }
        }
        return false;
    }

    private void mapJavadoc(final BodyDeclaration bodyDecl, final CSMember member) {
        final Javadoc javadoc = bodyDecl.getJavadoc();
        if (null == javadoc) {
            return;
        }

        mapJavadocTags(javadoc, member);
    }

    private boolean processDocumentationOverlay(CSMember node) {
        if (node instanceof CSTypeDeclaration) {
            return processTypeDocumentationOverlay((CSTypeDeclaration) node);
        }
        return processMemberDocumentationOverlay(node);
    }

    private boolean processMemberDocumentationOverlay(CSMember node) {
        String overlay = documentationOverlay().forMember(currentTypeQName(), node.signature());
        return processDocumentationOverlay(node, overlay);
    }

    private String currentTypeQName() {
        return qualifiedName(_currentType);
    }

    private boolean processTypeDocumentationOverlay(CSTypeDeclaration node) {
        String overlay = documentationOverlay().forType(qualifiedName(node));
        return processDocumentationOverlay(node, overlay);
    }

    private boolean processDocumentationOverlay(CSMember node, String overlay) {
        if (null == overlay) {
            return false;
        }
        node.addDoc(new CSDocTextOverlay(overlay.trim()));
        return true;
    }

    private DocumentationOverlay documentationOverlay() {
        return _configuration.documentationOverlay();
    }

    private String qualifiedName(CSTypeDeclaration node) {
        if (currentNamespace() == null) {
            return node.name();
        }
        return currentNamespace() + "." + node.name();
    }

    private String currentNamespace() {
        return _compilationUnit.namespace();
    }

    private void mapJavadocTags(final Javadoc javadoc, final CSMember member) {
        for (Object tag : javadoc.tags()) {
            try {
                TagElement element = (TagElement) tag;
                String tagName = element.getTagName();
                if (null == tagName) {
                    mapJavadocSummary(member, element);
                } else {
                    processTagElement(member, element);
                }
            } catch (Exception x) {
                warning((ASTNode) tag, x.getMessage());
                x.printStackTrace();
            }
        }
    }

    private void processTagElement(final CSMember member, TagElement element) {
        if (processSemanticallySignificantTagElement(member, element)) {
            return;
        }
        if (!isConversionTag(element.getTagName())) {
            member.addDoc(mapTagElement(member, element));
        } else if (isAttributeAnnotation(element)) {
            processAttribute(member, element);
        } else if (isNewAnnotation(element)) {
            member.setNewModifier(true);
        }
    }

    private boolean isAttributeAnnotation(TagElement element) {
        return element.getTagName().equals(SharpenAnnotations.SHARPEN_ATTRIBUTE);
    }

    private boolean isNewAnnotation(TagElement element) {
        return element.getTagName().equals(SharpenAnnotations.SHARPEN_NEW);
    }

    private void processAttribute(CSMember member, TagElement element) {
        String attrType = mappedTypeName(JavadocUtility.singleTextFragmentFrom(element));
        CSAttribute attribute = new CSAttribute(attrType);
        member.addAttribute(attribute);
    }

    private boolean processSemanticallySignificantTagElement(CSMember member, TagElement element) {
        if (element.getTagName().equals("@deprecated")) {
            member.removeAttribute("System.Obsolete");
            member.removeAttribute("System.ObsoleteAttribute");
            member.removeAttribute("Obsolete");
            member.removeAttribute("ObsoleteAttribute");
            member.addAttribute(obsoleteAttributeFromDeprecatedTagElement(element));
            return true;
        }
        return false;
    }

    private CSAttribute obsoleteAttributeFromDeprecatedTagElement(TagElement element) {

        CSAttribute attribute = new CSAttribute(mappedTypeName("System.ObsoleteAttribute"));
        if (element.fragments().isEmpty()) {
            return attribute;
        }
        attribute.addArgument(new CSStringLiteralExpression(toLiteralStringForm(getWholeText(element))));
        return attribute;
    }

    private String getWholeText(TagElement element) {
        StringBuilder builder = new StringBuilder();

        for (ASTNode fragment : (List<ASTNode>) element.fragments()) {
            if (fragment instanceof TextElement) {
                TextElement textElement = (TextElement) fragment;
                String text = textElement.getText();
                appendWithSpaceIfRequired(builder, text);
            } else if (fragment instanceof TagElement) {
                builder.append(getWholeText((TagElement) fragment));
            } else if (fragment instanceof MethodRef) {
                builder.append(mapCRefTarget(fragment));
            } else if (fragment instanceof MemberRef) {
                builder.append(mapCRefTarget(fragment));
            } else if (fragment instanceof Name) {
                builder.append(mapCRefTarget(fragment));
            } else {
                break;
            }
        }
        return builder.toString().trim();
    }

    private void appendWithSpaceIfRequired(StringBuilder builder, String text) {
        if (builder.length() > 0 && builder.charAt(builder.length() - 1) != ' ' && text.startsWith(" ") == false) {
            builder.append(" ");
        }
        builder.append(text);
    }

    private String toLiteralStringForm(String s) {
        // TODO: deal with escaping sequences here
        return "@\"" + s.replace("\"", "\"\"") + "\"";
    }

    private boolean isConversionTag(String tagName) {
        return tagName.startsWith("@sharpen.");
    }

    private void processPartialTagElement(AbstractTypeDeclaration node, CSTypeDeclaration member) {
        TagElement element = javadocTagFor(node, SharpenAnnotations.SHARPEN_PARTIAL);
        if (null == element)
            return;
        ((CSTypeDeclaration) member).partial(true);
    }

    private TagElement javadocTagFor(PackageDeclaration node, final String withName) {
        return JavadocUtility.getJavadocTag(node, withName);
    }

    private TagElement javadocTagFor(BodyDeclaration node, final String withName) {
        return JavadocUtility.getJavadocTag(node, withName);
    }

    private void mapJavadocSummary(final CSMember member, TagElement element) {
        List<String> summary = getFirstSentence(element);
        if (null != summary) {
            CSDocTagNode summaryNode = new CSDocTagNode("summary");
            for (String fragment : summary) {
                summaryNode.addFragment(new CSDocTextNode(fragment));
            }
            member.addDoc(summaryNode);
            CSDocNode remarksNode = createTagNode(member, "remarks", element, false);
            if (!summaryEqualsRemarks(summary, remarksNode))
                member.addDoc(remarksNode);
        } else {
            member.addDoc(createTagNode(member, "summary", element, false));
        }
    }

    private boolean summaryEqualsRemarks(List<String> summaryList, CSDocNode remarksNode) {
        if (!(remarksNode instanceof CSDocTagNode)) {
            return false;
        }

        String summary = "";

        for (String str : summaryList) {
            summary += str.trim();
        }

        String remarksStr = "";

        CSDocTagNode remarks = (CSDocTagNode) remarksNode;
        for (CSDocNode node : remarks.fragments()) {
            if (!(node instanceof CSDocTextNode)) {
                return false;
            }

            CSDocTextNode remarksDoc = (CSDocTextNode) node;
            remarksStr += remarksDoc.text().trim();
        }

        return summary.equalsIgnoreCase(remarksStr);
    }

    private List<String> getFirstSentence(TagElement element) {
        List<String> fragments = new LinkedList<String>();
        for (Object fragment : element.fragments()) {
            if (fragment instanceof TextElement) {
                TextElement textElement = (TextElement) fragment;
                String text = textElement.getText();
                int index = findSentenceClosure(text);
                if (index > -1) {
                    fragments.add(text.substring(0, index + 1));
                    return fragments;
                } else {
                    fragments.add(text);
                }
            } else {
                break;
            }
        }
        return null;
    }

    private int findSentenceClosure(String text) {
        Matcher matcher = SUMMARY_CLOSURE_PATTERN.matcher(text);
        return matcher.find() ? matcher.start() : -1;
    }

    private CSDocNode mapTagElement(CSMember member, TagElement element) {
        String tagName = element.getTagName();
        if (TagElement.TAG_PARAM.equals(tagName)) {
            return mapTagParam(member, element);
        } else if (TagElement.TAG_RETURN.equals(tagName)) {
            return createTagNode(member, "returns", element, false);
        } else if (TagElement.TAG_LINK.equals(tagName)) {
            return mapTagLink(member, element);
        } else if (TagElement.TAG_THROWS.equals(tagName)) {
            return mapTagThrows(member, element);
        } else if (TagElement.TAG_SEE.equals(tagName)) {
            return mapTagWithCRef(member, "seealso", element);
        } else if (TagElement.TAG_CODE.equals(tagName)) {
            if (element.fragments().size() == 1) {
                if (element.fragments().get(0) instanceof TextElement) {
                    return mapSingleTextElementCodeTagNode(member, element);
                }
            }

            return createTagNode(member, "c", element, true);
        }
        return createTagNode(member, tagName.substring(1), element, false);
    }

    // Used for emitting <see langword="true"/>, etc.
    // http://www.ewoodruff.us/xmlcommentsguide/html/983fed56-321c-4daf-af16-e3169b28ffcd.htm
    private static final Set<String> LANGUAGE_KEYWORDS = new HashSet<String>();

    static {
        LANGUAGE_KEYWORDS.add("true");
        LANGUAGE_KEYWORDS.add("false");
        LANGUAGE_KEYWORDS.add("null");
        LANGUAGE_KEYWORDS.add("static");
        LANGUAGE_KEYWORDS.add("abstract");
    }

    private CSDocNode mapSingleTextElementCodeTagNode(CSMember member, TagElement element) {
        TextElement fragment = (TextElement) element.fragments().get(0);
        String word = fragment.getText().trim();

        // {@code true} --> <see langword="true"/>
        if (LANGUAGE_KEYWORDS.contains(word)) {
            CSDocTagNode node = new CSDocTagNode("see");
            node.addAttribute("langword", word);
            return node;
        }

        // {@code foo} --> <paramref name="foo"/>
        if (member instanceof CSMethodBase) {
            CSMethodBase method = (CSMethodBase) member;
            boolean isParameterName = false;
            for (CSVariableDeclaration parameter : method.parameters()) {
                if (parameter.name().equals(word)) {
                    isParameterName = true;
                    break;
                }
            }

            if (isParameterName) {
                CSDocTagNode node = new CSDocTagNode("paramref");
                node.addAttribute("name", word);
                return node;
            }
        }

        return createTagNode(member, "c", element, true);
    }

    private CSDocNode mapTagThrows(CSMember member, TagElement element) {
        return mapTagWithCRef(member, "exception", element);
    }

    private CSDocNode mapTagLink(CSMember member, TagElement element) {
        return mapTagWithCRef(member, "see", element);
    }

    private CSDocNode mapTagWithCRef(CSMember member, String tagName, TagElement element) {
        final List fragments = element.fragments();
        if (fragments.isEmpty()) {
            return invalidTagWithCRef(member, element, tagName, element);
        }
        final ASTNode linkTarget = (ASTNode) fragments.get(0);
        String cref = mapCRefTarget(linkTarget);
        if (null == cref) {
            return invalidTagWithCRef(member, linkTarget, tagName, element);
        }
        CSDocTagNode node = newTagWithCRef(tagName, cref);
        if (fragments.size() > 1) {
            if (isLinkWithSimpleLabel(fragments, linkTarget)) {
                node.addTextFragment(unqualifiedName(cref));
            } else {
                collectFragments(member, node, fragments, 1);
            }
        }
        return node;
    }

    private ASTNode documentedNodeAttachedTo(TagElement element) {
        ASTNode attachedToNode = element;
        while (attachedToNode instanceof TagElement || attachedToNode instanceof Javadoc) {
            attachedToNode = attachedToNode.getParent();
        }
        return attachedToNode;
    }

    private CSDocNode invalidTagWithCRef(CSMember member, final ASTNode linkTarget, String tagName, TagElement element) {
        warning(linkTarget, "Tag '" + element.getTagName() + "' demands a valid cref target.");
        return createTagNode(member, tagName, element, false);
    }

    private CSDocTagNode newTagWithCRef(String tagName, String cref) {
        CSDocTagNode node = new CSDocTagNode(tagName);
        node.addAttribute("cref", cref);
        return node;
    }

    private boolean isLinkWithSimpleLabel(List<ASTNode> fragments, final ASTNode linkTarget) {
        if (fragments.size() != 2)
            return false;
        if (!JavadocUtility.isTextFragment(fragments, 1))
            return false;
        final String link = linkTarget.toString();
        final String label = JavadocUtility.textFragment(fragments, 1);
        return label.equals(link) || label.equals(unqualifiedName(link));
    }

    private String mapCRefTarget(final ASTNode crefTarget) {
        return new CRefBuilder(crefTarget).build();
    }

    private CSDocNode mapTagParam(CSMember member, TagElement element) {

        List fragments = element.fragments();

        if (!(fragments.get(0) instanceof SimpleName))
            return new CSDocTagNode("?");
        SimpleName name = (SimpleName) fragments.get(0);
        if (null == name.resolveBinding()) {
            warning(name, "Parameter '" + name + "' not found.");
        }

        CSDocTagNode param = isPropertyNode(documentedNodeAttachedTo(element))
                ? new CSDocTagNode("value")
                : newCSDocTag(fixIdentifierNameFor(identifier(name)));

        collectFragments(member, param, fragments, 1);
        return param;
    }

    private CSDocTagNode newCSDocTag(final String paramName) {
        CSDocTagNode param;
        param = new CSDocTagNode("param");
        param.addAttribute("name", paramName);
        return param;
    }

    private boolean isPropertyNode(ASTNode node) {
        if (node.getNodeType() != ASTNode.METHOD_DECLARATION) {
            return false;
        }

        return isProperty((MethodDeclaration) node);
    }

    private String fixIdentifierNameFor(String identifier) {
        return removeAtSign(identifier);
    }

    private String removeAtSign(String identifier) {
        return identifier.startsWith("@")
                ? identifier.substring(1)
                : identifier;
    }

    private void collectFragments(CSMember member, CSDocTagNode node, List fragments, int index) {
        for (int i = index; i < fragments.size(); ++i) {
            node.addFragment(mapTagElementFragment(member, (ASTNode) fragments.get(i), false));
        }
    }

    private CSDocNode mapTextElement(TextElement element, boolean escapeText) {
        String text = element.getText();
        if (escapeText) {
            text = text.replace("<", "&lt;").replace(">", "&gt;");
        } else if (HTML_ANCHOR_PATTERN.matcher(text).find()) {
            warning(element, "Caution: HTML anchors can result in broken links. Consider using @link instead.");
        }
        return new CSDocTextNode(text);
    }

    private CSDocNode createTagNode(CSMember member, String tagName, TagElement element, boolean escapeText) {
        CSDocTagNode summary = new CSDocTagNode(tagName);
        for (Object f : element.fragments()) {
            summary.addFragment(mapTagElementFragment(member, (ASTNode) f, escapeText));
        }
        return summary;
    }

    private CSDocNode mapTagElementFragment(CSMember member, ASTNode node, boolean escapeText) {
        switch (node.getNodeType()) {
            case ASTNode.TAG_ELEMENT:
                return mapTagElement(member, (TagElement) node);
            case ASTNode.TEXT_ELEMENT:
                return mapTextElement((TextElement) node, escapeText);
        }
        warning(node, "Documentation node not supported: " + node.getClass() + ": " + node);
        return new CSDocTextNode(node.toString());
    }

    public boolean visit(FieldDeclaration node) {
        if (SharpenAnnotations.hasIgnoreAnnotation(node)) {
            return false;
        }

        ITypeBinding fieldType = node.getType().resolveBinding();
        CSTypeReferenceExpression typeName = mappedTypeReference(fieldType);
        CSVisibility visibility = mapVisibility(node);
        if (((VariableDeclarationFragment) node.fragments().get(0)).resolveBinding().getDeclaringClass().isInterface())
            visibility = CSVisibility.Public;

        for (Object item : node.fragments()) {
            VariableDeclarationFragment fragment = (VariableDeclarationFragment) item;
            ITypeBinding saved = pushExpectedType(fieldType);
            CSField field = mapFieldDeclarationFragment(node, fragment, typeName, visibility);

            popExpectedType(saved);
            adjustVisibility(fieldType, field);

            CSTypeDeclaration enclosing = _currentType;
            if (_currentType.isInterface() && _currentAuxillaryType != null) {
                enclosing = _currentAuxillaryType;
            }

            if (isStatic(field) && hasStaticHelpersAnnotation(node.getParent())) {
                enclosing = staticType((TypeDeclaration) node.getParent());
            }
            enclosing.addMember(field);
        }

        return false;
    }

    private CSField mapFieldDeclarationFragment(FieldDeclaration node, VariableDeclarationFragment fragment,
                                                CSTypeReferenceExpression fieldType, CSVisibility fieldVisibility) {
        if (fragment.getExtraDimensions() > 0) {
            fieldType = new CSArrayTypeReference(fieldType, fragment.getExtraDimensions());
        }
        String fieldName = fieldName(fragment);
        fieldName = _resolver.resolveRename(fragment.resolveBinding(), fieldName);

        CSField field = new CSField(fieldName, fieldType, fieldVisibility, mapFieldInitializer(fragment));
        if (isConstField(node, fragment)) {
            field.addModifier(CSFieldModifier.Const);
        } else {
            processFieldModifiers(field, node.getModifiers());
        }
        mapAnnotations(node, field);
        mapDocumentation(node, field);
        return field;
    }

    private void mapAnnotations(BodyDeclaration node, CSMember member) {
        for (Object m : node.modifiers()) {
            if (!(m instanceof Annotation)) {
                continue;
            }
            if (isIgnoredAnnotation((Annotation) m)) {
                continue;
            }
            if (m instanceof MarkerAnnotation) {
                mapMarkerAnnotation((MarkerAnnotation) m, member);
            }
        }
    }

    private boolean isIgnoredAnnotation(Annotation m) {
        return _configuration.isIgnoredType(qualifiedName(m.resolveAnnotationBinding().getAnnotationType()));
    }

    private void mapMarkerAnnotation(MarkerAnnotation annotation, CSMember member) {
        final IAnnotationBinding binding = annotation.resolveAnnotationBinding();
        final CSAttribute attribute = new CSAttribute(mappedTypeName(binding.getAnnotationType()));
        member.addAttribute(attribute);
    }

    protected String fieldName(VariableDeclarationFragment fragment) {
        return identifier(fragment.getName());
    }

    protected String fieldName(EnumConstantDeclaration enumConstant) {
        return identifier(enumConstant.getName());
    }

    protected CSExpression mapFieldInitializer(VariableDeclarationFragment fragment) {
        return mapExpression(fragment.getInitializer());
    }

    private boolean isConstField(FieldDeclaration node, VariableDeclarationFragment fragment) {
        //
        if (fragment.resolveBinding().getDeclaringClass().isInterface())
            return true;
        return Modifier.isFinal(node.getModifiers()) && isSupportedConstantType(node.getType()) &&
                hasConstValue(fragment) && Modifier.isStatic(node.getModifiers());
    }

    private boolean isSupportedConstantType(Type type) {
        return type.isPrimitiveType()
                || type.resolveBinding().getQualifiedName().equals(String.class.getCanonicalName());
    }

    private boolean hasConstValue(VariableDeclarationFragment fragment) {
        return null != fragment.resolveBinding().getConstantValue();
    }

    private void processFieldModifiers(CSField field, int modifiers) {
        if (Modifier.isStatic(modifiers)) {
            field.addModifier(CSFieldModifier.Static);
        }
        if (Modifier.isFinal(modifiers)) {
            field.addModifier(CSFieldModifier.Readonly);
        }
        if (Modifier.isTransient(modifiers)) {
            field.addAttribute(new CSAttribute(mappedTypeName("System.NonSerialized")));
        }
        if (Modifier.isVolatile(modifiers)) {
            field.addModifier(CSFieldModifier.Volatile);
        }

    }

    private boolean isDestructor(MethodDeclaration node) {
        return node.getName().toString().equals("finalize");
    }

    public boolean visit(Initializer node) {
        if (Modifier.isStatic(node.getModifiers())) {
            CSConstructor ctor = new CSConstructor(CSConstructorModifier.Static);
            _currentType.addMember(ctor);
            visitBodyDeclarationBlock(node, node.getBody(), ctor);
        } else {
            _instanceInitializers.add(node);
        }
        return false;
    }

    public boolean visit(MethodDeclaration node) {
        nullablePrimitivesVariables.clear();

        if (SharpenAnnotations.hasIgnoreAnnotation(node) || isRemoved(node)) {
            return false;
        }

        if (isEvent(node)) {
            processEventDeclaration(node);
            return false;
        }

        if (isMappedToProperty(node)) {
            processMappedPropertyDeclaration(node);
            return false;
        }

        if (isTaggedAsProperty(node)) {
            processPropertyDeclaration(node);
            return false;
        }

        if (isIndexer(node)) {
            processIndexerDeclaration(node);
            return false;
        }

        processMethodDeclaration(node);

        return false;
    }

    private void processIndexerDeclaration(MethodDeclaration node) {
        processPropertyDeclaration(node, CSProperty.INDEXER);
    }

    private boolean isIndexer(MethodDeclaration node) {
        return isTaggedDeclaration(node, SharpenAnnotations.SHARPEN_INDEXER);
    }

    private boolean isRemoved(MethodDeclaration node) {
        return hasRemoveAnnotation(node) || isRemoved(node.resolveBinding());
    }

    private boolean hasRemoveAnnotation(BodyDeclaration node) {
        return containsJavadoc(node, SharpenAnnotations.SHARPEN_REMOVE);
    }

    private boolean isRemoved(final IMethodBinding binding) {
        return _configuration.isRemoved(qualifiedName(binding));
    }

    private boolean hasStaticHelpersAnnotation(ASTNode node) {
        return node instanceof BodyDeclaration && containsJavadoc((BodyDeclaration) node, SharpenAnnotations.SHARPEN_STATIC_HELPERS);
    }

    public static boolean containsJavadoc(BodyDeclaration node, final String tag) {
        return JavadocUtility.containsJavadoc(node, tag);
    }

    private void processPropertyDeclaration(MethodDeclaration node) {
        processPropertyDeclaration(node, propertyName(node));
    }

    private void processMappedPropertyDeclaration(MethodDeclaration node) {
        processPropertyDeclaration(node, mappedMethodName(node));
    }

    private void processPropertyDeclaration(MethodDeclaration node, final String name) {
        mapPropertyDeclaration(node, producePropertyFor(node, name));
    }

    private CSProperty producePropertyFor(MethodDeclaration node, final String name) {
        CSProperty existingProperty = findProperty(node, name);
        if (existingProperty != null) {
            return existingProperty;
        }
        CSProperty property = newPropertyFor(node, name);
        _currentType.addMember(property);
        return property;
    }

    private CSProperty findProperty(MethodDeclaration node, final String name) {
        CSMember existingProperty = _currentType.getMember(name);
        if (existingProperty != null) {
            if (!(existingProperty instanceof CSProperty)) {
                throw new IllegalArgumentException(sourceInformation(node) + ": Previously declared member redeclared as property.");
            }
        }
        return (CSProperty) existingProperty;
    }

    private CSProperty mapPropertyDeclaration(MethodDeclaration node, CSProperty property) {
        final CSBlock block = mapBody(node);

        if (isGetter(node)) {
            property.getter(block);
        } else {
            property.setter(block);
            mapImplicitSetterParameter(node, property);
        }
        mapMetaMemberAttributes(node, property);
        mapParameters(node, property);
        return property;
    }

    private void mapImplicitSetterParameter(MethodDeclaration node, CSProperty property) {
        final String parameterName = parameter(node, 0).getName().toString();
        if (parameterName.equals("value")) {
            return;
        }

        property.setter().addStatement(0,
                newVariableDeclarationExpression(parameterName, property.type(), new CSReferenceExpression("value")));
    }

    private CSDeclarationExpression newVariableDeclarationExpression(final String name,
                                                                     final CSTypeReferenceExpression type, final CSReferenceExpression initializer) {
        return new CSDeclarationExpression(
                new CSVariableDeclaration(name, type, initializer));
    }

    private CSProperty newPropertyFor(MethodDeclaration node, final String propName) {
        final CSTypeReferenceExpression propertyType = isGetter(node)
                ? mappedReturnType(node)
                : mappedTypeReference(lastParameter(node).getType(), false);
        return new CSProperty(propName, propertyType);
    }

    private CSBlock mapBody(MethodDeclaration node) {
        final CSBlock block = new CSBlock();
        processBlock(node, node.getBody(), block);
        return block;
    }

    private boolean isGetter(MethodDeclaration node) {
        return !"void".equals(node.getReturnType2().toString());
    }

    private SingleVariableDeclaration lastParameter(MethodDeclaration node) {
        return parameter(node, node.parameters().size() - 1);
    }

    private String propertyName(MethodDeclaration node) {
        return my(Annotations.class).annotatedPropertyName(node);
    }

    private String propertyName(IMethodBinding binding) {
        return propertyName((MethodDeclaration) declaringNode(binding));
    }

    private boolean isProperty(BodyDeclaration node) {
        return isTaggedAsProperty(node)
                || isMappedToProperty(node);
    }

    private boolean isTaggedAsProperty(BodyDeclaration node) {
        return isTaggedDeclaration(node, SharpenAnnotations.SHARPEN_PROPERTY);
    }

    private boolean isTaggedDeclaration(BodyDeclaration node, final String tag) {
        return effectiveAnnotationFor(node, tag) != null;
    }

    private void processMethodDeclaration(MethodDeclaration node) {
        if (isDestructor(node)) {
            mapMethodParts(node, new CSDestructor());
            return;
        }

        if (node.isConstructor()) {
            mapMethodParts(node, new CSConstructor());
            return;
        }

        CSMethod method = new CSMethod(mappedMethodDeclarationName(node));
        method.returnType(mappedReturnType(node));
        method.modifier(mapMethodModifier(node));
        mapTypeParameters(node.typeParameters(), method);
        mapMethodParts(node, method);

        if (_configuration.junitConversion() && isLegacyTestFixture(node.resolveBinding().getDeclaringClass())) {
            if (method.name().startsWith("Test") && method.visibility() == CSVisibility.Public)
                method.addAttribute(new CSAttribute("NUnit.Framework.Test"));
            if (isLegacyTestFixtureClass(node.resolveBinding().getDeclaringClass().getSuperclass())) {
                if (method.name().equals("SetUp")) {
                    method.addAttribute(new CSAttribute("NUnit.Framework.SetUp"));
                    method.modifier(CSMethodModifier.Virtual);
                    cleanBaseSetupCalls(method);
                } else if (method.name().equals("TearDown")) {
                    method.addAttribute(new CSAttribute("NUnit.Framework.TearDown"));
                    method.modifier(CSMethodModifier.Virtual);
                    cleanBaseSetupCalls(method);
                }
            }
        }
    }

    private void cleanBaseSetupCalls(CSMethod method) {
        ArrayList<CSStatement> toDelete = new ArrayList<CSStatement>();
        for (CSStatement st : method.body().statements()) {
            if (st instanceof CSExpressionStatement) {
                CSExpressionStatement es = (CSExpressionStatement) st;
                if (es.expression() instanceof CSMethodInvocationExpression) {
                    CSMethodInvocationExpression mie = (CSMethodInvocationExpression) es.expression();
                    if (mie.expression() instanceof CSMemberReferenceExpression) {
                        CSMemberReferenceExpression mr = (CSMemberReferenceExpression) mie.expression();
                        if ((mr.expression() instanceof CSBaseExpression) && (mr.name().equals("SetUp") || mr.name().equals("TearDown")))
                            toDelete.add(st);
                    }
                }
            }
        }
        for (CSStatement st : toDelete)
            method.body().removeStatement(st);
    }

    private void mapMethodParts(MethodDeclaration node, CSMethodBase method) {

        if (isStatic(method) && hasStaticHelpersAnnotation(node.getParent())) {
            staticType((TypeDeclaration) node.getParent()).addMember(method);
        } else {
            _currentType.addMember(method);
        }

        method.startPosition(node.getStartPosition());
        method.isVarArgs(node.isVarargs());
        mapParameters(node, method);
        mapAnnotations(node, method);
        mapDocumentation(node, method);
        if (isEnumConstructor(node)) {
            implementEnumBaseConstructorInvocation((CSConstructor) method);
        }
        visitBodyDeclarationBlock(node, node.getBody(), method);

        IMethodBinding overriden = getOverridedMethod(node);
        if (isEnumConstructor(node)) {
            method.visibility(CSVisibility.Private);
        } else if (!node.isConstructor() && overriden != null) {
            CSVisibility vis = mapVisibility(overriden.getModifiers());
            if (vis == CSVisibility.ProtectedInternal && !overriden.getDeclaringClass().isFromSource())
                vis = CSVisibility.Protected;
            method.visibility(vis);
        } else if (node.resolveBinding().getDeclaringClass().isInterface()) {
            method.visibility(CSVisibility.Public);
        } else {
            mapVisibility(node, method);
        }
    }

    private String mappedMethodDeclarationName(MethodDeclaration node) {
        final String mappedName = mappedMethodName(node);
        if (null == mappedName || 0 == mappedName.length() || mappedName.contains(".")) {
            return methodName(node.getName().toString());
        }
        return mappedName;
    }

    private void mapParameters(MethodDeclaration node, CSParameterized method) {
        if (method instanceof CSMethod) {
            mapMethodParameters(node, (CSMethod) method);
            return;
        }
        if (isEnumConstructor(node)) {
            addEnumBaseConstructorParameters(method);
        }
        for (Object p : node.parameters()) {
            mapParameter((SingleVariableDeclaration) p, method);
        }
    }

    private void mapParameter(SingleVariableDeclaration parameter, CSParameterized method) {
        if (method instanceof CSMethod) {
            IVariableBinding vb = parameter.resolveBinding();
            ITypeBinding[] arguments = vb.getType().getTypeArguments();
            if (arguments.length > 0 && !isMacroType(findDeclaringNode(vb.getType()))) {
                CSMethod csMethod = (CSMethod) method;
                CSTypeReference type = new CSTypeReference(mappedTypeName(vb.getType()));

                // we need to check that generic class is not Class<?>
                if (!BindingUtils.isJavaLangClass(vb.getType())) {
                    int argNameNum = 0;
                    for (ITypeBinding argument : arguments) {
                        if (argument.getName().startsWith("?")) {
                            String genericArg = "_T" + argNameNum++;
                            CSTypeParameter methodTypeParameter = new CSTypeParameter(genericArg);
                            for (ITypeBinding binding : mapTypeParameterExtendedType(argument)) {
                                methodTypeParameter.superClass(mappedTypeReference(binding));
                            }
                            csMethod.addTypeParameter(methodTypeParameter);

                            type.addTypeArgument(new CSTypeReference(genericArg));
                        } else {
                            type.addTypeArgument(mappedTypeReference(argument));
                        }
                    }
                }

                method.addParameter(new CSVariableDeclaration(identifier(vb.getName()), type));
                return;
            }
        }
        method.addParameter(createParameter(parameter));
    }

    List<ITypeBinding> mapTypeParameterExtendedType(ITypeBinding tb) {
        List<ITypeBinding> result = new ArrayList<>();
        ITypeBinding superc = tb.getSuperclass();
        if (superc != null && !superc.getQualifiedName().equals("java.lang.Object") && !superc.getQualifiedName().equals("java.lang.Enum<?>")) {
            result.add(superc);
        }
        Collections.addAll(result, tb.getInterfaces());
        return result;
    }

    private void mapMethodParameters(MethodDeclaration node, CSMethod method) {
        for (Object o : node.parameters()) {
            SingleVariableDeclaration p = (SingleVariableDeclaration) o;
            ITypeBinding parameterType = p.getType().resolveBinding();
            if (!isGenericRuntimeParameterIdiom(node.resolveBinding(), parameterType)) {
                mapParameter(p, method);
            }
        }
    }

    private CSTypeReferenceExpression genericRuntimeTypeIdiomType(ITypeBinding parameterType) {
        return mappedTypeReference(parameterType.getTypeArguments()[0]);
    }

    private boolean isGenericRuntimeParameterIdiom(IMethodBinding method, ITypeBinding parameterType) {
        if (!parameterType.isParameterizedType()) {
            return false;
        }
        if (!"java.lang.Class".equals(qualifiedName(parameterType))) {
            return false;
        }
        // detecting if the T in Class<T> comes from the method itself
        final ITypeBinding classTypeArgument = parameterType.getTypeArguments()[0];
        return classTypeArgument.getDeclaringMethod() == method;
    }

    private CSTypeReferenceExpression mappedReturnType(MethodDeclaration node) {
        if (hasYieldMarker(node)) {
            compilationUnit().addUsing(new CSUsing("System.Collections"));
            return new CSTypeReference("IEnumerator");

        }
        IMethodBinding overriden = getOverridedMethod(node);
        if (overriden != null)
            return mappedTypeReference(overriden.getReturnType());
        return mappedTypeReference(node.getReturnType2(), false);
    }

    private boolean hasYieldMarker(ASTNode node) {
        while (node != null && !(node instanceof MethodDeclaration)) {
            node = node.getParent();
        }
        if (node != null) {
            for (Object type : ((MethodDeclaration)node).thrownExceptionTypes()) {
                BodyDeclaration declaration = findDeclaringNode(((SimpleType) type).resolveBinding());
                if (declaration != null && containsJavadoc(declaration, SharpenAnnotations.SHARPEN_YIELD)) {
                    return true;
                }
            }
        }
        return false;
    }

    private boolean isInvocationWithYieldMarker(MethodInvocation node) {
        final BodyDeclaration method = declaringNode(node.resolveMethodBinding());
        return hasYieldMarker(method);
    }

    private void processInvocationWithYieldMarker(MethodInvocation node) {
        processOrdinaryMethodInvocation(node);
        pushExpression(new CSYieldReturnStatement(popExpression()));
    }

    private boolean isInvocationWithYieldMarker(SuperMethodInvocation node) {
        final BodyDeclaration method = declaringNode(node.resolveMethodBinding());
        return hasYieldMarker(method);
    }

    private void processEventDeclaration(MethodDeclaration node) {
        CSTypeReference eventHandlerType = new CSTypeReference(getEventHandlerTypeName(node));
        CSEvent event = createEventFromMethod(node, eventHandlerType);
        mapMetaMemberAttributes(node, event);
        if (_currentType.isInterface())
            return;

        VariableDeclarationFragment field = getEventBackingField(node);
        CSField backingField = (CSField) _currentType.getMember(field.getName().toString());
        backingField.type(eventHandlerType);

        // clean field
        backingField.initializer(null);
        backingField.removeModifier(CSFieldModifier.Readonly);

        final CSBlock addBlock = createEventBlock(backingField, "System.Delegate.Combine");
        String onAddMethod = getEventOnAddMethod(node);
        if (onAddMethod != null) {
            addBlock.addStatement(new CSMethodInvocationExpression(new CSReferenceExpression(onAddMethod)));
        }
        event.setAddBlock(addBlock);
        event.setRemoveBlock(createEventBlock(backingField, "System.Delegate.Remove"));
    }

    private String getEventOnAddMethod(MethodDeclaration node) {
        final TagElement onAddTag = javadocTagFor(node, SharpenAnnotations.SHARPEN_EVENT_ON_ADD);
        if (null == onAddTag)
            return null;
        return methodName(JavadocUtility.singleTextFragmentFrom(onAddTag));
    }

    private String getEventHandlerTypeName(MethodDeclaration node) {
        final String eventArgsType = getEventArgsType(node);
        return buildEventHandlerTypeName(node, eventArgsType);
    }

    private void mapMetaMemberAttributes(MethodDeclaration node, CSMetaMember metaMember) {
        mapVisibility(node, metaMember);
        metaMember.modifier(mapMethodModifier(node));
        mapDocumentation(node, metaMember);
    }

    private CSBlock createEventBlock(CSField backingField, String delegateMethod) {
        CSBlock block = new CSBlock();
        block.addStatement(new CSInfixExpression("=", new CSReferenceExpression(backingField.name()),
                new CSCastExpression(backingField.type(), new CSMethodInvocationExpression(new CSReferenceExpression(
                        delegateMethod), new CSReferenceExpression(backingField.name()), new CSReferenceExpression(
                        "value")))));
        return block;
    }

    private static final class CheckVariableUseVisitor extends ASTVisitor {

        private final IVariableBinding _var;
        private boolean _used;

        private CheckVariableUseVisitor(IVariableBinding var) {
            this._var = var;
        }

        @Override
        public boolean visit(SimpleName name) {
            IBinding binding = name.resolveBinding();
            if (binding == null) {
                return false;
            }
            if (binding.equals(_var)) {
                _used = true;
            }

            return false;
        }

        public boolean used() {
            return _used;
        }
    }

    private static final class FieldAccessFinder extends ASTVisitor {
        public IBinding field;

        @Override
        public boolean visit(SimpleName node) {
            field = node.resolveBinding();
            return false;
        }
    }

    private VariableDeclarationFragment getEventBackingField(MethodDeclaration node) {
        FieldAccessFinder finder = new FieldAccessFinder();
        node.accept(finder);
        return findDeclaringNode(finder.field);
    }

    private CSEvent createEventFromMethod(MethodDeclaration node, CSTypeReference eventHandlerType) {
        String eventName = methodName(node);
        CSEvent event = new CSEvent(eventName, eventHandlerType);
        _currentType.addMember(event);
        return event;
    }

    private String methodName(MethodDeclaration node) {
        return methodName(node.getName().toString());
    }

    private String unqualifiedName(String typeName) {
        int index = typeName.lastIndexOf('.');
        if (index < 0)
            return typeName;
        return typeName.substring(index + 1);
    }

    private String buildEventHandlerTypeName(ASTNode node, String eventArgsTypeName) {
        if (!eventArgsTypeName.endsWith("EventArgs")) {
            warning(node, SharpenAnnotations.SHARPEN_EVENT + " type name must end with 'EventArgs'");
            return eventArgsTypeName + "EventHandler";
        }

        return "System.EventHandler<" + eventArgsTypeName + ">";
    }

    private String getEventArgsType(MethodDeclaration node) {
        TagElement tag = eventTagFor(node);
        if (null == tag)
            return null;
        return mappedTypeName(JavadocUtility.singleTextFragmentFrom(tag));
    }

    private TagElement eventTagFor(MethodDeclaration node) {
        return effectiveAnnotationFor(node, SharpenAnnotations.SHARPEN_EVENT);
    }

    private TagElement effectiveAnnotationFor(BodyDeclaration node, final String annotation) {
        return my(Annotations.class).effectiveAnnotationFor(node, annotation);
    }

    @SuppressWarnings("unchecked")
    private <T extends ASTNode> T findDeclaringNode(IBinding binding) {
        return (T) my(Bindings.class).findDeclaringNode(binding);
    }

    private void visitBodyDeclarationBlock(BodyDeclaration node, Block block, CSMethodBase method) {
        CSMethodBase saved = _currentMethod;
        _currentMethod = method;

        processDisableTags(node, method);
        processBlock(node, block, method.body());
        _currentMethod = saved;
    }

    private void processDisableTags(PackageDeclaration packageDeclaration, CSNode csNode) {
        TagElement tag = javadocTagFor(packageDeclaration, SharpenAnnotations.SHARPEN_IF);
        if (null == tag)
            return;

        csNode.addEnclosingIfDef(JavadocUtility.singleTextFragmentFrom(tag));
    }

    private void processDisableTags(BodyDeclaration node, CSNode csNode) {
        TagElement tag = javadocTagFor(node, SharpenAnnotations.SHARPEN_IF);
        if (null == tag)
            return;

        csNode.addEnclosingIfDef(JavadocUtility.singleTextFragmentFrom(tag));
    }

    private void processBlock(BodyDeclaration node, Block block, final CSBlock targetBlock) {
        if (containsJavadoc(node, SharpenAnnotations.SHARPEN_REMOVE_FIRST)) {
            block.statements().remove(0);
        }

        BodyDeclaration savedDeclaration = _currentBodyDeclaration;
        _currentBodyDeclaration = node;

        if (Modifier.isSynchronized(node.getModifiers())) {
            CSLockStatement lock = new CSLockStatement(node.getStartPosition(), getLockTarget(node));
            targetBlock.addStatement(lock);
            visitBlock(lock.body(), block);
        } else {
            visitBlock(targetBlock, block);
        }
        _currentBodyDeclaration = savedDeclaration;
    }

    private CSExpression getLockTarget(BodyDeclaration node) {
        return Modifier.isStatic(node.getModifiers()) ? new CSTypeofExpression(new CSTypeReference(_currentType.name()))
                : new CSThisExpression();
    }

    public boolean visit(ConstructorInvocation node) {
        addChainedConstructorInvocation(new CSThisExpression(), node.arguments());
        return false;
    }

    private void addChainedConstructorInvocation(CSExpression target, List arguments) {
        CSConstructorInvocationExpression cie = new CSConstructorInvocationExpression(target);
        mapArguments(cie, arguments);
        ((CSConstructor) _currentMethod).chainedConstructorInvocation(cie);
    }

    public boolean visit(SuperConstructorInvocation node) {
        if (null != node.getExpression()) {
            notImplemented(node);
        }
        addChainedConstructorInvocation(new CSBaseExpression(), node.arguments());
        return false;
    }

    private <T extends ASTNode> void visitBlock(CSBlock block, T node) {
        if (null == node) {
            return;
        }

        CSBlock saved = _currentBlock;
        _currentBlock = block;

        _currentContinueLabel = null;

        node.accept(this);
        _currentBlock = saved;
    }

    public boolean visit(VariableDeclarationExpression node) {
        pushExpression(new CSDeclarationExpression(createVariableDeclaration((VariableDeclarationFragment) node
                .fragments().get(0))));
        return false;
    }

    public boolean visit(VariableDeclarationStatement node) {
        for (Object f : node.fragments()) {
            VariableDeclarationFragment variable = (VariableDeclarationFragment) f;
            CSVariableDeclaration variableDeclaration = createVariableDeclaration(variable);
            if (variable.getInitializer() != null && BindingUtils.hasWildcardInGenerics(variable.getInitializer().resolveTypeBinding())) {
                variableDeclaration.type(new CSTypeReference("var"));
            }
            addStatement(new CSDeclarationStatement(node.getStartPosition(), variableDeclaration));
        }
        return false;
    }

    private CSVariableDeclaration createVariableDeclaration(VariableDeclarationFragment variable) {
        IVariableBinding binding = variable.resolveBinding();
        ITypeBinding saved = pushExpectedType(binding.getType());
        CSExpression initializer = mapExpression(variable.getInitializer());
        popExpectedType(saved);
        return createVariableDeclaration(binding, initializer);
    }

    private CSVariableDeclaration createVariableDeclaration(IVariableBinding binding, CSExpression initializer) {
        String name = binding.getName();
        if (_blockVariables.size() > 0) {
            if (_blockVariables.peek().contains(name)) {
                int count = 1;
                while (_blockVariables.peek().contains(name + "_" + count)) {
                    count++;
                }
                _renamedVariables.peek().put(name, name + "_" + count);
                name = name + "_" + count;
            }
            _localBlockVariables.peek().add(name);
            for (Set<String> s : _blockVariables)
                s.add(name);
        }
        if (isGetOfNullableIndexer(initializer)) {
            nullablePrimitivesVariables.add(binding.getKey());
            CSTypeReference type = new CSTypeReference(mappedTypeName(binding.getType()) + "?");
            return new CSVariableDeclaration(identifier(name), type, initializer);
        }
        return new CSVariableDeclaration(identifier(name), mappedTypeReference(binding.getType()), initializer);
    }

    private boolean isGetOfNullableIndexer(CSExpression initializer) {
        if (!(initializer instanceof CSMethodInvocationExpression)) {
            return false;
        }
        CSNode expression = ((CSMethodInvocationExpression) initializer).expression();
        if (!(expression instanceof CSReferenceExpression)) {
            return false;
        }
        return ((CSReferenceExpression) expression).name().equals("GetOrNullable");
    }

    public boolean visit(ExpressionStatement node) {
        if (isRemovedMethodInvocation(node.getExpression())) {
            return false;
        }

        addStatement(new CSExpressionStatement(node.getStartPosition(), mapExpression(node.getExpression())));
        return false;
    }

    private boolean isRemovedMethodInvocation(Expression expression) {
        if (!(expression instanceof MethodInvocation)) {
            return false;
        }

        MethodInvocation invocation = (MethodInvocation) expression;
        return isTaggedMethodInvocation(invocation, SharpenAnnotations.SHARPEN_REMOVE)
                || isRemoved(invocation.resolveMethodBinding());

    }

    public boolean visit(IfStatement node) {
        Expression expression = node.getExpression();

        Object constValue = constValue(expression);
        if (null != constValue) {
            // dead branch elimination
            if (isTrue(constValue)) {
                node.getThenStatement().accept(this);
            } else {
                if (null != node.getElseStatement()) {
                    node.getElseStatement().accept(this);
                }
            }
        } else {
            CSIfStatement stmt = new CSIfStatement(node.getStartPosition(), mapExpression(expression));
            visitBlock(stmt.trueBlock(), node.getThenStatement());
            visitBlock(stmt.falseBlock(), node.getElseStatement());
            addStatement(stmt);
        }
        return false;
    }

    private boolean isTrue(Object constValue) {
        return (Boolean) constValue;
    }

    private Object constValue(Expression expression) {
        switch (expression.getNodeType()) {
            case ASTNode.PREFIX_EXPRESSION:
                return constValue((PrefixExpression) expression);
            case ASTNode.SIMPLE_NAME:
            case ASTNode.QUALIFIED_NAME:
                return constValue((Name) expression);
        }
        return null;
    }

    public Object constValue(PrefixExpression expression) {
        if (PrefixExpression.Operator.NOT == expression.getOperator()) {
            Object value = constValue(expression.getOperand());
            if (null != value) {
                return isTrue(value) ? Boolean.FALSE : Boolean.TRUE;
            }
        }
        return null;
    }

    public Object constValue(Name expression) {
        IBinding binding = expression.resolveBinding();
        if (IBinding.VARIABLE == binding.getKind()) {
            return ((IVariableBinding) binding).getConstantValue();
        }
        return null;
    }

    public boolean visit(final WhileStatement node) {
        consumeContinueLabel(new Function<CSBlock>() {
            public CSBlock apply() {
                CSWhileStatement stmt = new CSWhileStatement(node.getStartPosition(), mapExpression(node.getExpression()));
                visitBlock(stmt.body(), node.getBody());
                addStatement(stmt);
                return stmt.body();
            }
        });
        return false;
    }

    public boolean visit(final DoStatement node) {
        consumeContinueLabel(new Function<CSBlock>() {
            public CSBlock apply() {
                CSDoStatement stmt = new CSDoStatement(node.getStartPosition(), mapExpression(node.getExpression()));
                visitBlock(stmt.body(), node.getBody());
                addStatement(stmt);
                return stmt.body();
            }
        });
        return false;
    }

    public boolean visit(TryStatement node) {
        CSTryStatement stmt = new CSTryStatement(node.getStartPosition());
        CSBlock body = stmt.body();
        for (Object resource : node.resources()) {
            VariableDeclarationExpression expression = (VariableDeclarationExpression) resource;
            expression.accept(this);
            CSUsingStatement using = new CSUsingStatement(expression.getStartPosition(), popExpression());
            body.addStatement(using);
            body = using.body();
        }
        visitBlock(body, node.getBody());
        for (Object o : node.catchClauses()) {
            CatchClause clause = (CatchClause) o;
            if (!_configuration.isIgnoredExceptionType(qualifiedName(clause.getException().getType().resolveBinding()))) {
                CSCatchClause catchClause = mapCatchClause(clause);
                catchClause.parent(stmt);
                stmt.addCatchClause(catchClause);
            }
        }
        if (null != node.getFinally()) {
            CSBlock finallyBlock = new CSBlock();
            visitBlock(finallyBlock, node.getFinally());
            stmt.finallyBlock(finallyBlock);
        }

        if (null != stmt.finallyBlock() || !stmt.catchClauses().isEmpty()) {
            addStatement(stmt);
        } else {
            _currentBlock.addAll(stmt.body());
        }
        return false;
    }

    private CSCatchClause mapCatchClause(CatchClause node) {
        IVariableBinding oldExceptionVariable = _currentExceptionVariable;
        _currentExceptionVariable = node.getException().resolveBinding();
        try {
            CheckVariableUseVisitor check = new CheckVariableUseVisitor(_currentExceptionVariable);
            node.getBody().accept(check);

            // The exception variable is declared in a new scope
            pushScope();

            CSCatchClause clause;
            if (isEmptyCatch(node, check)) {
                clause = new CSCatchClause();
            } else {
                clause = new CSCatchClause(createVariableDeclaration(_currentExceptionVariable, null));
            }
            clause.anonymous(!check.used());
            visitBlock(clause.body(), node.getBody());
            return clause;
        } finally {
            _currentExceptionVariable = oldExceptionVariable;
            popScope();
        }
    }

    private boolean isEmptyCatch(CatchClause clause, CheckVariableUseVisitor check) {
        if (check.used())
            return false;
        return isThrowable(clause.getException().resolveBinding().getType());
    }

    private boolean isThrowable(ITypeBinding declaringClass) {
        return "java.lang.Throwable".equals(qualifiedName(declaringClass));
    }

    public boolean visit(ThrowStatement node) {
        addStatement(mapThrowStatement(node));
        return false;
    }

    private CSThrowStatement mapThrowStatement(ThrowStatement node) {
        Expression exception = node.getExpression();
        if (isCurrentExceptionVariable(exception)) {
            return new CSThrowStatement(node.getStartPosition(), null);
        }
        return new CSThrowStatement(node.getStartPosition(), mapExpression(exception));
    }

    private boolean isCurrentExceptionVariable(Expression exception) {
        if (!(exception instanceof SimpleName)) {
            return false;
        }
        return ((SimpleName) exception).resolveBinding() == _currentExceptionVariable;
    }

    public boolean visit(BreakStatement node) {
        SimpleName labelName = node.getLabel();
        if (labelName != null) {
            addStatement(new CSGotoStatement(node.getStartPosition(), breakLabel(labelName.getIdentifier())));
            return false;
        }
        addStatement(new CSBreakStatement(node.getStartPosition()));
        return false;
    }

    public boolean visit(ContinueStatement node) {
        SimpleName labelName = node.getLabel();
        if (labelName != null) {
            addStatement(new CSGotoStatement(node.getStartPosition(), continueLabel(labelName.getIdentifier())));
            return false;
        }
        addStatement(new CSContinueStatement(node.getStartPosition()));
        return false;
    }

    public boolean visit(SynchronizedStatement node) {
        CSLockStatement stmt = new CSLockStatement(node.getStartPosition(), mapExpression(node.getExpression()));
        visitBlock(stmt.body(), node.getBody());
        addStatement(stmt);
        return false;
    }

    public boolean visit(ReturnStatement node) {
        if (node.getExpression() == null && hasYieldMarker(node)) {
            addStatement(new CSYieldBreakStatement(node.getStartPosition()));
        } else {
            addStatement(new CSReturnStatement(node.getStartPosition(), mapExpression(node.getExpression())));
        }
        return false;
    }

    public boolean visit(NumberLiteral node) {

        String token = node.getToken();
        CSExpression literal = new CSNumberLiteralExpression(token);

        if (expectingType("byte") && token.startsWith("-")) {
            literal = uncheckedCast("byte", literal);
        } else if (token.startsWith("0x")) {
            if (token.endsWith("l") || token.endsWith("L")) {
                literal = uncheckedCast("long", literal);
            } else {
                literal = uncheckedCast("int", literal);
            }

        } else if (token.startsWith("0") && token.indexOf('.') == -1 && Character.isDigit(token.charAt(token.length() - 1))) {
            try {
                int n = Integer.parseInt(token, 8);
                if (n != 0)
                    literal = new CSNumberLiteralExpression("0x" + Integer.toHexString(n));
            } catch (NumberFormatException ignore) {
            }
        }

        pushExpression(literal);
        return false;
    }

    private CSUncheckedExpression uncheckedCast(String type, CSExpression expression) {
        return new CSUncheckedExpression(new CSCastExpression(new CSTypeReference(type), new CSParenthesizedExpression(
                expression)));
    }

    public boolean visit(StringLiteral node) {
        String value = node.getLiteralValue();
        if (value != null && value.length() == 0) {
            pushExpression(new CSReferenceExpression("string.Empty"));
        } else {
            pushExpression(new CSStringLiteralExpression(fixEscapedNumbers(node.getEscapedValue())));
        }
        return false;
    }

    String fixEscapedNumbers(String literal) {
        StringBuilder s = new StringBuilder();
        for (int n = 0; n < literal.length(); n++) {
            if (literal.charAt(n) == '\\') {
                int i = n + 1;
                if (i < literal.length() && literal.charAt(i) == '\\') {
                    s.append("\\\\");
                    n = i;
                    continue;
                }
                while (i < literal.length() && Character.isDigit(literal.charAt(i)))
                    i++;
                if (i != n + 1) {
                    int num = Integer.parseInt(literal.substring(n + 1, i));
                    s.append("\\x").append(Integer.toHexString(num));
                    n = i - 1;
                    continue;
                }
            }
            s.append(literal.charAt(n));
        }
        return s.toString();
    }

    public boolean visit(CharacterLiteral node) {
        CSExpression expr = new CSCharLiteralExpression(node.getEscapedValue());
        if (expectingType("byte")) {
            expr = new CSCastExpression(new CSTypeReference("byte"), new CSParenthesizedExpression(
                    expr));
        }
        pushExpression(expr);
        return false;
    }

    private boolean expectingType(String name) {
        return (_currentExpectedType != null && _currentExpectedType.getName().equals(name));
    }

    public boolean visit(NullLiteral node) {
        if (node.getParent() instanceof CastExpression && ((CastExpression) node.getParent()).getType().resolveBinding().isTypeVariable()) {
            pushExpression(
                    new CSPrefixExpression("default",
                            new CSParenthesizedExpression(
                                    new CSTypeReference(mappedTypeName(((CastExpression) node.getParent()).getType().resolveBinding()))
                            )
                    )
            );
        } else {
            pushExpression(new CSNullLiteralExpression());
        }
        return false;
    }

    public boolean visit(BooleanLiteral node) {
        pushExpression(new CSBoolLiteralExpression(node.booleanValue()));
        return false;
    }

    public boolean visit(ThisExpression node) {
        pushExpression(new CSThisExpression());
        return false;
    }

    public boolean visit(ArrayAccess node) {
        pushExpression(new CSIndexedExpression(mapExpression(node.getArray()), mapExpression(node.getIndex())));
        return false;
    }

    public boolean visit(ArrayCreation node) {
        ITypeBinding saved = pushExpectedType(node.getType().getElementType().resolveBinding());
        if (node.dimensions().size() > 1) {
            if (null != node.getInitializer()) {
                notImplemented(node);
            }
            pushExpression(unfoldMultiArrayCreation(node));
        } else {
            pushExpression(mapSingleArrayCreation(node));
        }
        popExpectedType(saved);
        return false;
    }

    /**
     * Unfolds java multi array creation shortcut "new String[2][3][2]" into
     * explicitly array creation "new string[][][] { new string[][] { new
     * string[2], new string[2], new string[2] }, new string[][] { new
     * string[2], new string[2], new string[2] } }"
     */
    private CSArrayCreationExpression unfoldMultiArrayCreation(ArrayCreation node) {
        ArrayType type = node.getType();
        if (node.getParent() instanceof CastExpression &&
                ((CastExpression) node.getParent()).getType() instanceof ArrayType &&
                (type.getElementType().resolveBinding().isRawType() || ((ArrayType) (((CastExpression) node.getParent()).getType())).getElementType().resolveBinding().isTypeVariable())
        ) {
            // convert explicit casts for raw array creation to generic array creation
            type = (ArrayType) ((CastExpression) node.getParent()).getType();
        }
        return unfoldMultiArray(type.getElementType(), type.getDimensions() - 1, node.dimensions(), 0);
    }

    private CSArrayCreationExpression unfoldMultiArray(Type type, int typeDimension, List dimensions, int dimensionIndex) {
        final CSArrayCreationExpression expression = new CSArrayCreationExpression(mappedArrayReference(type, typeDimension));
        expression.initializer(new CSArrayInitializerExpression());
        int length = resolveIntValue(dimensions.get(dimensionIndex));
        if (dimensionIndex < lastIndex(dimensions) - 1) {
            for (int i = 0; i < length; ++i) {
                expression.initializer().addExpression(
                        unfoldMultiArray(type, typeDimension - 1, dimensions, dimensionIndex + 1));
            }
        } else {
            Expression innerLength = (Expression) dimensions.get(dimensionIndex + 1);
            CSTypeReferenceExpression innerType = mappedArrayReference(type, typeDimension - 1);
            for (int i = 0; i < length; ++i) {
                expression.initializer().addExpression(
                        new CSArrayCreationExpression(innerType, mapExpression(innerLength)));
            }
        }
        return expression;
    }

    private int lastIndex(List<?> dimensions) {
        return dimensions.size() - 1;
    }

    private int resolveIntValue(Object expression) {
        return ((Number) ((Expression) expression).resolveConstantExpressionValue()).intValue();
    }

    private CSArrayCreationExpression mapSingleArrayCreation(ArrayCreation node) {
        ArrayType type = node.getType();

        if (node.getParent() instanceof CastExpression &&
                ((CastExpression) node.getParent()).getType() instanceof ArrayType &&
                (type.getElementType().resolveBinding().isRawType() || ((ArrayType) (((CastExpression) node.getParent()).getType())).getElementType().resolveBinding().isTypeVariable())
        ) {
            // convert explicit casts for raw array creation to generic array creation
            type = (ArrayType) ((CastExpression) node.getParent()).getType();
        }

        CSArrayCreationExpression expression = new CSArrayCreationExpression(mappedArrayReference(type.getElementType(), type.getDimensions() - 1));
        if (!node.dimensions().isEmpty()) {
            expression.length(mapExpression((Expression) node.dimensions().get(0)));
        }
        expression.initializer(mapArrayInitializer(node));
        return expression;
    }

    private CSArrayInitializerExpression mapArrayInitializer(ArrayCreation node) {
        return (CSArrayInitializerExpression) mapExpression(node.getInitializer());
    }

    public boolean visit(ArrayInitializer node) {
        if (isImplicitelyTypedArrayInitializer(node)) {
            CSArrayCreationExpression ace = new CSArrayCreationExpression(mappedTypeReference(node.resolveTypeBinding()
                    .getComponentType()));
            ITypeBinding saved = pushExpectedType(node.resolveTypeBinding().getElementType());
            ace.initializer(mapArrayInitializer(node));
            popExpectedType(saved);
            pushExpression(ace);
            return false;
        }

        pushExpression(mapArrayInitializer(node));
        return false;
    }

    private CSArrayInitializerExpression mapArrayInitializer(ArrayInitializer node) {
        CSArrayInitializerExpression initializer = new CSArrayInitializerExpression();
        for (Object e : node.expressions()) {
            initializer.addExpression(mapExpression((Expression) e));
        }
        return initializer;
    }

    private boolean isImplicitelyTypedArrayInitializer(ArrayInitializer node) {
        return !(node.getParent() instanceof ArrayCreation);
    }

    @Override
    public boolean visit(EnhancedForStatement node) {
        pushScope();
        CSForEachStatement stmt = new CSForEachStatement(node.getStartPosition(), mapExpression(node.getExpression()));
        if (BindingUtils.hasWildcardInGenerics(node.getParameter().getType().resolveBinding()) || BindingUtils.hasWildcardInGenerics(node.getExpression().resolveTypeBinding())) {
            stmt.variable(new CSVariableDeclaration(node.getParameter().getName().getIdentifier(), new CSTypeReference("var")));
        } else {
            stmt.variable(createParameter(node.getParameter()));
        }
        visitBlock(stmt.body(), node.getBody());
        addStatement(stmt);
        return false;
    }

    @Override
    public void endVisit(EnhancedForStatement node) {
        popScope();
    }

    public boolean visit(final ForStatement node) {
        pushScope();
        consumeContinueLabel(new Function<CSBlock>() {
            public CSBlock apply() {
                ArrayList<CSExpression> initializers = new ArrayList<CSExpression>();
                for (Object i : node.initializers()) {
                    if (i instanceof VariableDeclarationExpression) {
                        VariableDeclarationExpression v = (VariableDeclarationExpression) i;
                        for (Object j : v.fragments()) {
                            VariableDeclarationFragment fragment = (VariableDeclarationFragment) j;
                            if (initializers.size() == 0) {
                                CSExpression c = mapFragement(fragment);
                                initializers.add(c);
                            } else {
                                CSInfixExpression csInfix = new CSInfixExpression("=", mapExpression(fragment.getName()), mapExpression(fragment.getInitializer()));
                                initializers.add(csInfix);
                            }
                        }
                    } else {
                        CSExpression c = mapExpression((Expression) i);
                        initializers.add(c);
                    }
                }
                CSForStatement stmt = new CSForStatement(node.getStartPosition(), mapExpression(node.getExpression()));
                for (CSExpression i : initializers) {
                    stmt.addInitializer(i);
                }
                for (Object u : node.updaters()) {
                    stmt.addUpdater(mapExpression((Expression) u));
                }
                visitBlock(stmt.body(), node.getBody());
                addStatement(stmt);
                return stmt.body();
            }
        });
        return false;
    }

    @Override
    public void endVisit(ForStatement node) {
        popScope();
    }

    protected CSExpression mapFragement(VariableDeclarationFragment v) {
        pushExpression(new CSDeclarationExpression(createVariableDeclaration(v)));
        return popExpression();
    }

    private void consumeContinueLabel(Function<CSBlock> func) {
        CSLabelStatement label = _currentContinueLabel;
        _currentContinueLabel = null;
        CSBlock body = func.apply();
        if (label != null) {
            body.addStatement(label);
        }
    }

    private boolean isSwitchCaseClosingStatement(CSStatement stmt) {
        return stmt instanceof CSThrowStatement
                || stmt instanceof CSReturnStatement
                || stmt instanceof CSBreakStatement
                || stmt instanceof CSYieldBreakStatement
                || stmt instanceof CSGotoStatement
                || stmt instanceof CSContinueStatement;
    }

    public boolean visit(SwitchStatement node) {
        _currentContinueLabel = null;
        CSBlock saved = _currentBlock;

        ITypeBinding switchType = node.getExpression().resolveTypeBinding();
        CSSwitchStatement mappedNode;
        if (switchType.isEnum()) {
            CSExpression enumExpression = mapExpression(node.getExpression());
            CSMethodInvocationExpression ordinalInvocation = new CSMethodInvocationExpression(new CSMemberReferenceExpression(enumExpression, "ordinal"));
            mappedNode = new CSSwitchStatement(node.getStartPosition(), ordinalInvocation);
        } else {
            mappedNode = new CSSwitchStatement(node.getStartPosition(), mapExpression(node.getExpression()));
        }
        addStatement(mappedNode);

        CSCaseClause defaultClause = null;
        CSCaseClause current = null;
        CSBlock openCaseBlock = null;
        _currentBlock = null;
        for (ASTNode element : Types.<Iterable<ASTNode>>cast(node.statements())) {
            if (ASTNode.SWITCH_CASE == element.getNodeType()) {
                if (null == current) {
                    if (_currentBlock != null) {
                        List<CSStatement> stats = _currentBlock.statements();
                        CSStatement lastStmt = stats.size() > 0 ? stats.get(stats.size() - 1) : null;
                        if (!isSwitchCaseClosingStatement(lastStmt))
                            openCaseBlock = _currentBlock;
                    }
                    current = new CSCaseClause();
                    mappedNode.addCase(current);
                    _currentBlock = current.body();
                }
                SwitchCase sc = (SwitchCase) element;
                if (sc.isDefault()) {
                    defaultClause = current;
                    current.isDefault(true);
                    if (openCaseBlock != null)
                        openCaseBlock.addStatement(new CSGotoStatement(Integer.MIN_VALUE, "default"));
                } else {
                    CSExpression caseExpression;
                    if (switchType.isEnum()) {
                        EnumDeclaration enumDecl = findDeclaringNode(switchType);
                        int ordinal = ASTUtility.getEnumOrdinal(enumDecl, ((SimpleName) sc.getExpression()).getIdentifier());
                        caseExpression = new CSNumberLiteralExpression(ordinal + "");
                    } else {
                        ITypeBinding stype = pushExpectedType(switchType);
                        caseExpression = mapExpression(sc.getExpression());
                        popExpectedType(stype);
                    }
                    current.addExpression(caseExpression);
                    if (openCaseBlock != null) {
                        openCaseBlock.addStatement(new CSGotoStatement(Integer.MIN_VALUE, caseExpression));
                    }
                }
                openCaseBlock = null;
            } else {
                element.accept(this);
                current = null;
            }
        }

        if (_currentBlock != null) {
            List<CSStatement> stats = _currentBlock.statements();
            CSStatement lastStmt = stats.size() > 0 ? stats.get(stats.size() - 1) : null;
            if (lastStmt == null || !isSwitchCaseClosingStatement(lastStmt))
                openCaseBlock = _currentBlock;
        }

        if (openCaseBlock != null)
            openCaseBlock.addStatement(new CSBreakStatement(Integer.MIN_VALUE));

        if (null != defaultClause) {
            List<CSStatement> stats = defaultClause.body().statements();

            CSStatement lastStmt = stats.size() > 0 ? stats.get(stats.size() - 1) : null;
            if (!isSwitchCaseClosingStatement(lastStmt)) {
                defaultClause.body().addStatement(new CSBreakStatement(Integer.MIN_VALUE));
            }
        }

        _currentBlock = saved;
        return false;
    }

    public boolean visit(CastExpression node) {
        pushExpression(new CSCastExpression(mappedTypeReference(node.getType(), false), mapExpression(node.getExpression())));
        // Make all byte casts unchecked
        if (node.getType().resolveBinding().getName().equals("byte"))
            pushExpression(new CSUncheckedExpression(popExpression()));
        return false;
    }

    public boolean visit(PrefixExpression node) {
        CSExpression expr;
        expr = new CSPrefixExpression(node.getOperator().toString(), mapExpression(node.getOperand()));
        if (expectingType("byte") && node.getOperator() == PrefixExpression.Operator.MINUS) {
            expr = uncheckedCast("byte", expr);
        }
        pushExpression(expr);
        return false;
    }

    public boolean visit(PostfixExpression node) {
        pushExpression(new CSPostfixExpression(node.getOperator().toString(), mapExpression(node.getOperand())));
        return false;
    }

    public boolean visit(InfixExpression node) {

        CSExpression left = mapExpression(node.getLeftOperand());
        CSExpression right = mapExpression(node.getRightOperand());
        String type = node.getLeftOperand().resolveTypeBinding().getQualifiedName();
        if (node.getOperator() == InfixExpression.Operator.RIGHT_SHIFT_UNSIGNED) {
            if (type.equals("byte")) {
                pushExpression(new CSInfixExpression(">>", left, right));
            } else {
                CSExpression cast = new CSCastExpression(new CSTypeReference("u" + type), left);
                cast = new CSParenthesizedExpression(cast);
                CSExpression shiftResult = new CSInfixExpression(">>", cast, right);
                shiftResult = new CSParenthesizedExpression(shiftResult);
                pushExpression(new CSCastExpression(new CSTypeReference(type), shiftResult));
            }
            return false;
        }
        if (type.equals("byte") && (node.getOperator() == InfixExpression.Operator.LESS || node.getOperator() == InfixExpression.Operator.LESS_EQUALS)) {
            left = new CSCastExpression(new CSTypeReference("sbyte"), left);
            left = new CSParenthesizedExpression(left);
        }
        String operator = node.getOperator().toString();
        pushExpression(new CSInfixExpression(operator, left, right));
        pushExtendedOperands(operator, node);
        return false;
    }

    private void pushExtendedOperands(String operator, InfixExpression node) {
        for (Object x : node.extendedOperands()) {
            pushExpression(new CSInfixExpression(operator, popExpression(), mapExpression((Expression) x)));
        }
    }

    public boolean visit(ParenthesizedExpression node) {
        pushExpression(new CSParenthesizedExpression(mapExpression(node.getExpression())));
        return false;
    }

    public boolean visit(ConditionalExpression node) {
        pushExpression(new CSConditionalExpression(mapExpression(node.getExpression()), mapExpression(node
                .getThenExpression()), mapExpression(node.getElseExpression())));
        return false;
    }

    public boolean visit(InstanceofExpression node) {
        CSTypeReferenceExpression rightExp = mappedTypeReference(node.getRightOperand().resolveBinding(), false, true);
        if (rightExp instanceof CSTypeReference && !((CSTypeReference) rightExp).typeArguments().isEmpty()) {
            CSMethodInvocationExpression mie = new CSMethodInvocationExpression(new CSReferenceExpression(
                    methodName(_configuration.getRuntimeTypeName() + ".InstanceOf")
            ));
            mie.addArgument(mapExpression(node.getLeftOperand()));
            mie.addArgument(new CSTypeofExpression(rightExp));
            pushExpression(mie);
        } else {
            pushExpression(new CSInfixExpression("is",
                    mapExpression(node.getLeftOperand()),
                    rightExp
            ));
        }
        return false;
    }

    @Override
    public boolean visit(AssertStatement node) {
        List<CSExpression> args = new ArrayList<CSExpression>();
        args.add(mapExpression(node.getExpression()));
        if (node.getMessage() != null) {
            args.add(mapExpression(node.getMessage()));
        }

        CSExpression assertMethod = new CSMemberReferenceExpression(new CSTypeReference("System.Diagnostics.Debug"), "Assert");
        addStatement(new CSExpressionStatement(node.getStartPosition(), new CSMethodInvocationExpression(assertMethod, args.toArray(new CSExpression[args.size()]))));
        return false;
    }

    @Override
    public boolean visit(EnumConstantDeclaration node) {
        if (SharpenAnnotations.hasIgnoreAnnotation(node)) {
            return false;
        }

        ITypeBinding fieldType = ((EnumDeclaration) node.getParent()).resolveBinding();
        CSTypeReferenceExpression typeName = mappedTypeReference(fieldType);
        CSVisibility visibility = mapVisibility(node);

        ITypeBinding saved = pushExpectedType(fieldType);

        CSMethodInvocationExpression initializer;
        Configuration.MemberMapping mappedConstructor = effectiveMappingFor(node.resolveConstructorBinding());
        if (null == mappedConstructor) {
            int ordinal = ASTUtility.getEnumOrdinal((EnumDeclaration) node.getParent(), node.getName().getIdentifier());
            initializer = new CSConstructorInvocationExpression(mappedTypeReference(fieldType));
            initializer.addArgument(new CSNumberLiteralExpression(ordinal + ""));
            initializer.addArgument(new CSStringLiteralExpression("\"" + node.getName().getIdentifier() + "\""));
        } else {
            final String mappedName = mappedConstructor.name;
            if (mappedName.length() == 0) {
                throw new UnsupportedOperationException();
            } else if (mappedName.startsWith("System.Convert.To")) {
                throw new UnsupportedOperationException();
            } else {
                initializer = new CSMethodInvocationExpression(new CSReferenceExpression(methodName(mappedName)));
            }
        }

        mapArguments(initializer, node.arguments());

        CSField field = new CSField(fieldName(node), typeName, visibility, initializer);
        field.addModifier(CSFieldModifier.Static);
        field.addModifier(CSFieldModifier.Readonly);
        mapAnnotations(node, field);
        mapDocumentation(node, field);

        popExpectedType(saved);
        adjustVisibility(fieldType, field);
        _currentType.addMember(field);

        return false;
    }

    public boolean visit(Assignment node) {
        Expression lhs = node.getLeftHandSide();
        Expression rhs = node.getRightHandSide();
        ITypeBinding lhsType = lhs.resolveTypeBinding();
        ITypeBinding saved = pushExpectedType(lhsType);

        if (node.getOperator() == Assignment.Operator.RIGHT_SHIFT_UNSIGNED_ASSIGN) {
            String type = lhsType.getQualifiedName();
            if ("byte".equals(type)) {
                pushExpression(new CSInfixExpression(">>", mapExpression(lhs), mapExpression(lhs
                        .resolveTypeBinding(), rhs)));
            } else {
                CSExpression mappedLhs = mapExpression(lhs);
                CSExpression cast = new CSCastExpression(new CSTypeReference("u" + type), mappedLhs);
                cast = new CSParenthesizedExpression(cast);
                CSExpression shiftResult = new CSInfixExpression(">>", cast, mapExpression(rhs));
                shiftResult = new CSParenthesizedExpression(shiftResult);
                shiftResult = new CSCastExpression(new CSTypeReference(type), shiftResult);
                pushExpression(new CSInfixExpression("=", mappedLhs, shiftResult));
            }
        } else {
            pushExpression(new CSInfixExpression(node.getOperator().toString(), mapExpression(lhs), mapExpression(lhs
                    .resolveTypeBinding(), rhs)));
        }
        popExpectedType(saved);
        return false;
    }

    private CSExpression mapExpression(ITypeBinding expectedType, Expression expression) {
        if (expectedType != null)
            return castIfNeeded(expectedType, expression.resolveTypeBinding(), mapExpression(expression));
        else
            return mapExpression(expression);
    }

    private CSExpression castIfNeeded(ITypeBinding expectedType, ITypeBinding actualType, CSExpression expression) {
        if (!_configuration.mapIteratorToEnumerator() && expectedType.getName().startsWith("Iterable<") && isGenericCollection(actualType)) {
            return new CSMethodInvocationExpression(new CSMemberReferenceExpression(expression, "AsIterable"));
        }
        if (expectedType != actualType && isSubclassOf(expectedType, actualType))
            return new CSCastExpression(mappedTypeReference(expectedType), expression);

        ITypeBinding charType = resolveWellKnownType("char");
        if (expectedType != charType)
            return expression;
        if (actualType == expectedType)
            return expression;
        return new CSCastExpression(mappedTypeReference(expectedType), expression);
    }

    private boolean isGenericCollection(ITypeBinding t) {
        return t.getName().startsWith("List<") || t.getName().startsWith("Set<");
    }

    private boolean isSubclassOf(ITypeBinding t, ITypeBinding tbase) {
        while (t != null) {
            if (t.isEqualTo(tbase))
                return true;
            t = t.getSuperclass();
        }
        return false;
    }

    public boolean visit(ClassInstanceCreation node) {
        if (null != node.getAnonymousClassDeclaration()) {
            node.getAnonymousClassDeclaration().accept(this);
            return false;
        }

        CSMethodInvocationExpression expression = mapConstructorInvocation(node);
        if (null == expression) {
            return false;
        }

        if (isNonStaticNestedTypeCreation(node)) {
            expression.addArgument(new CSThisExpression());
        }

        mapArguments(expression, node.arguments());
        pushExpression(expression);
        return false;
    }

    private boolean isNonStaticNestedTypeCreation(ClassInstanceCreation node) {
        return isNonStaticNestedType(node.resolveTypeBinding());
    }

    private CSMethodInvocationExpression mapConstructorInvocation(ClassInstanceCreation node) {
        Configuration.MemberMapping mappedConstructor = effectiveMappingFor(node.resolveConstructorBinding());
        if (null == mappedConstructor) {
            return new CSConstructorInvocationExpression(mappedTypeReference(node.resolveTypeBinding()));
        }
        final String mappedName = mappedConstructor.name;
        if (mappedName.length() == 0) {
            pushExpression(mapExpression((Expression) node.arguments().get(0)));
            return null;
        }
        if (mappedName.startsWith("System.Convert.To")) {
            if (optimizeSystemConvert(mappedName, node)) {
                return null;
            }
        }
        return new CSMethodInvocationExpression(new CSReferenceExpression(methodName(mappedName)));
    }

    private boolean optimizeSystemConvert(String mappedConstructor, ClassInstanceCreation node) {
        String typeName = _configuration.getConvertRelatedWellKnownTypeName(mappedConstructor);
        if (null != typeName) {
            assert 1 == node.arguments().size();
            Expression arg = (Expression) node.arguments().get(0);
            if (arg.resolveTypeBinding() == resolveWellKnownType(typeName)) {
                arg.accept(this);
                return true;
            }
        }
        return false;
    }

    public boolean visit(TypeLiteral node) {

        if (isReferenceToRemovedType(node.getType())) {
            pushExpression(new CSRemovedExpression(node.toString()));
            return false;
        }

        pushTypeOfExpression(mappedTypeReference(node.getType(), true));
        return false;
    }

    private boolean isReferenceToRemovedType(Type node) {
        BodyDeclaration typeDeclaration = findDeclaringNode(node.resolveBinding());
        if (null == typeDeclaration)
            return false;
        return hasRemoveAnnotation(typeDeclaration);
    }

    private void pushTypeOfExpression(CSTypeReferenceExpression type) {
        if (_configuration.nativeTypeSystem()) {
            pushExpression(new CSTypeofExpression(type));
        } else {
            pushGetClassForTypeExpression(type);
        }
    }

    private void pushGetClassForTypeExpression(final CSTypeReferenceExpression typeName) {
        CSMethodInvocationExpression mie = new CSMethodInvocationExpression(new CSReferenceExpression(
                methodName(_configuration.getRuntimeTypeName() + ".getClassForType")));
        mie.addArgument(new CSTypeofExpression(typeName));
        pushExpression(mie);
    }

    public boolean visit(MethodInvocation node) {

        IMethodBinding binding = originalMethodBinding(node.resolveMethodBinding());
        Configuration.MemberMapping mapping = mappingForInvocation(binding);

        if (null != mapping) {
            processMappedMethodInvocation(node, binding, mapping);
        } else {
            processUnmappedMethodInvocation(node);
        }

        return false;
    }

    public boolean visit(SuperMethodInvocation node) {
        if (null != node.getQualifier()) {
            notImplemented(node);
        }

        IMethodBinding binding = originalMethodBinding(node.resolveMethodBinding());
        Configuration.MemberMapping mapping = mappingForInvocation(binding);
        CSExpression target = new CSMemberReferenceExpression(new CSBaseExpression(), mappedMethodName(binding));

        if (mapping != null && mapping.kind != MemberKind.Method) {
            pushExpression(target);
            return false;
        }

        CSMethodInvocationExpression mie = new CSMethodInvocationExpression(target);
        mapArguments(mie, node.arguments());

        if (isInvocationWithYieldMarker(node)) {
            pushExpression(new CSYieldReturnStatement(mie));
            return false;
        }

        pushExpression(mie);
        return false;
    }

    private Configuration.MemberMapping mappingForInvocation(IMethodBinding binding) {
        Configuration.MemberMapping mapping = effectiveMappingFor(binding);

        if (null == mapping) {
            if (isIndexer(binding)) {
                mapping = new MemberMapping(null, MemberKind.Indexer);
            } else if (isTaggedMethodInvocation(binding, SharpenAnnotations.SHARPEN_EVENT)) {
                mapping = new MemberMapping(binding.getName(), MemberKind.Property);
            } else if (isTaggedMethodInvocation(binding, SharpenAnnotations.SHARPEN_PROPERTY)) {
                mapping = new MemberMapping(propertyName(binding), MemberKind.Property);
            }
        }
        return mapping;
    }

    private boolean isIndexer(final IMethodBinding binding) {
        return isTaggedMethod(binding, SharpenAnnotations.SHARPEN_INDEXER);
    }

    private boolean isTaggedMethod(final IMethodBinding binding, final String tag) {
        final BodyDeclaration declaration = declaringNode(binding);
        if (null == declaration) {
            return false;
        }
        return isTaggedDeclaration(declaration, tag);
    }

    private IMethodBinding originalMethodBinding(IMethodBinding binding) {
        IMethodBinding original = BindingUtils.findMethodDefininition(binding, my(CompilationUnit.class).getAST());
        if (null != original)
            return original;
        return binding;
    }

    private void processUnmappedMethodInvocation(MethodInvocation node) {

        if (isMappedEventSubscription(node)) {
            processMappedEventSubscription(node);
            return;
        }

        if (isEventSubscription(node)) {
            processEventSubscription(node);
            return;
        }

        if (isRemovedMethodInvocation(node)) {
            processRemovedInvocation(node);
            return;
        }

        if (isUnwrapInvocation(node)) {
            processUnwrapInvocation(node);
            return;
        }

        if (isMacro(node)) {
            processMacroInvocation(node);
            return;
        }

        if (isInvocationWithYieldMarker(node)) {
            processInvocationWithYieldMarker(node);
            return;
        }

        processOrdinaryMethodInvocation(node);
    }

    private boolean isMacro(MethodInvocation node) {
        return isTaggedMethodInvocation(node, SharpenAnnotations.SHARPEN_MACRO) ||
                my(Mappings.class).configuredMacroFor(node.resolveMethodBinding()) != null;
    }

    private void processMacroInvocation(MethodInvocation node) {
        IMethodBinding binding = node.resolveMethodBinding();
        String mappedMacro = my(Mappings.class).configuredMacroFor(binding);
        if (mappedMacro == null) {
            final MethodDeclaration declaration = (MethodDeclaration) declaringNode(binding);
            final TagElement macro = effectiveAnnotationFor(declaration, SharpenAnnotations.SHARPEN_MACRO);
            mappedMacro = JavadocUtility.singleTextFragmentFrom(macro);
        }
        final CSMacro code = new CSMacro(mappedMacro);
        ITypeBinding[] typeArguments = binding.getReturnType().getTypeArguments();
        if (mappedMacro.contains("$type")) {
            code.addVariable("type", mappedTypeReference(binding.getReturnType()));
        }

        for (int i = 0; i < typeArguments.length; i++) {
            ITypeBinding typeBinding = typeArguments[i];
            if (mappedMacro.contains("$type_" + i)) {
                code.addVariable("type_" + i, mappedTypeReference(typeBinding));
            }
        }
        if (mappedMacro.contains("$expression")) {
            code.addVariable("expression", mapExpression(node.getExpression()));
        }
        if (mappedMacro.contains("$arguments")) {
            code.addVariable("arguments", mapExpressions(node.arguments()));
        }
        List arguments = node.arguments();
        for (int i = 0; i < arguments.size(); i++) {
            if (mappedMacro.contains("$arg_" + i)) {
                code.addVariable("arg_" + i, mapExpression((Expression) arguments.get(i)));
            }
        }

        pushExpression(new CSMacroExpression(code));
    }

    private List<CSExpression> mapExpressions(List expressions) {
        final ArrayList<CSExpression> result = new ArrayList<CSExpression>(expressions.size());
        for (Object expression : expressions) {
            result.add(mapExpression((Expression) expression));
        }
        return result;
    }

    private boolean isUnwrapInvocation(MethodInvocation node) {
        return isTaggedMethodInvocation(node, SharpenAnnotations.SHARPEN_UNWRAP);
    }

    private void processUnwrapInvocation(MethodInvocation node) {
        final List arguments = node.arguments();
        if (arguments.size() != 1) {
            unsupportedConstruct(node, SharpenAnnotations.SHARPEN_UNWRAP + " only works against single argument methods.");
        }
        pushExpression(mapExpression((Expression) arguments.get(0)));
    }

    private void processOrdinaryMethodInvocation(MethodInvocation node) {
        IMethodBinding method = node.resolveMethodBinding();
        CSExpression targetExpression = mapMethodTargetExpression(node);
        if ((method.getModifiers() & Modifier.STATIC) != 0 && !(targetExpression instanceof CSTypeReferenceExpression) && node.getExpression() != null)
            targetExpression = mappedTypeReference(node.getExpression().resolveTypeBinding());

        String name = resolveTargetMethodName(targetExpression, node);
        CSExpression target = null == targetExpression
                ? new CSReferenceExpression(name)
                : new CSMemberReferenceExpression(targetExpression, name);
        CSMethodInvocationExpression mie = new CSMethodInvocationExpression(target);
        mapMethodInvocationArguments(mie, node);
        mapTypeArguments(mie, node);

        IMethodBinding base = getOverridedMethod(method);
        if (base != null && base.getReturnType() != method.getReturnType() && !(node.getParent() instanceof ExpressionStatement))
            pushExpression(new CSParenthesizedExpression(new CSCastExpression(mappedTypeReference(method.getReturnType()), mie)));
        else
            pushExpression(mie);
    }

    private String resolveTargetMethodName(CSExpression targetExpression, MethodInvocation node) {
        final IMethodBinding method = staticImportMethodBinding(node.getName(), _ast.imports());
        if (method != null && targetExpression == null) {
            return mappedTypeName(method.getDeclaringClass()) + "." + mappedMethodName(node.resolveMethodBinding());
        }
        return mappedMethodName(node.resolveMethodBinding());
    }

    private void mapTypeArguments(CSMethodInvocationExpression mie, MethodInvocation node) {
        for (Object o : node.typeArguments()) {
            mie.addTypeArgument(mappedTypeReference((Type) o, false));
        }
    }

    private void processMappedEventSubscription(MethodInvocation node) {

        final MethodInvocation event = (MethodInvocation) node.getExpression();
        final String eventArgsType = _configuration.mappedEvent(qualifiedName(event));
        final String eventHandlerType = buildEventHandlerTypeName(node, eventArgsType);
        mapEventSubscription(node, eventArgsType, eventHandlerType);
    }

    private void processRemovedInvocation(MethodInvocation node) {
        TagElement element = javadocTagFor(declaringNode(node.resolveMethodBinding()), SharpenAnnotations.SHARPEN_REMOVE);

        String exchangeValue = JavadocUtility.singleTextFragmentFrom(element);
        pushExpression(new CSReferenceExpression(exchangeValue));
    }

    private void mapMethodInvocationArguments(CSMethodInvocationExpression mie, MethodInvocation node) {
        final List arguments = node.arguments();
        final IMethodBinding actualMethod = node.resolveMethodBinding();
        final ITypeBinding[] actualTypes = actualMethod.getParameterTypes();
        final IMethodBinding originalMethod = actualMethod.getMethodDeclaration();
        final ITypeBinding[] originalTypes = originalMethod.getParameterTypes();
        for (int i = 0; i < arguments.size(); ++i) {
            final Expression arg = (Expression) arguments.get(i);
            if (i < originalTypes.length && isGenericRuntimeParameterIdiom(originalMethod, originalTypes[i])) {
                mie.addTypeArgument(genericRuntimeTypeIdiomType(actualTypes[i]));
            } else {
                addArgument(mie, arg, i < actualTypes.length ? actualTypes[i] : null);
            }
        }
        adjustJUnitArguments(mie, node);
    }

    private void adjustJUnitArguments(CSMethodInvocationExpression mie, MethodInvocation node) {
        if (!_configuration.junitConversion())
            return;
        ITypeBinding t = node.resolveMethodBinding().getDeclaringClass();
        if (t.getQualifiedName().equals("junit.framework.Assert") || t.getQualifiedName().equals("org.junit.Assert")) {
            String method = node.getName().getIdentifier();
            int np = -1;

            if (method.equals("assertTrue") || method.equals("assertFalse")
                    || method.equals("assertNull") || method.equals("assertNotNull"))
                np = 1;
            else if (method.equals("fail"))
                np = 0;
            else if (method.startsWith("assert"))
                np = 2;

            if (np == -1)
                return;

            if (mie.arguments().size() == np + 1) {
                // Move the comment argument to the end
                mie.addArgument(mie.arguments().get(0));
                mie.removeArgument(0);
            }

            if (method.equals("assertSame")) {
                boolean useEquals = false;
                final List arguments = node.arguments();
                for (int i = 0; i < arguments.size(); ++i) {
                    final Expression arg = (Expression) arguments.get(i);
                    ITypeBinding b = arg.resolveTypeBinding();
                    if (b.isEnum()) {
                        useEquals = true;
                        break;
                    }
                }
                if (useEquals) {
                    CSReferenceExpression mref = (CSReferenceExpression) mie.expression();
                    mref.name("NUnit.Framework.Assert.AreEqual");
                }
            }
        }
    }

    private void processEventSubscription(MethodInvocation node) {

        final MethodDeclaration addListener = (MethodDeclaration) declaringNode(node.resolveMethodBinding());
        assertValidEventAddListener(node, addListener);

        final MethodInvocation eventInvocation = (MethodInvocation) node.getExpression();

        final MethodDeclaration eventDeclaration = (MethodDeclaration) declaringNode(eventInvocation.resolveMethodBinding());
        mapEventSubscription(node, getEventArgsType(eventDeclaration), getEventHandlerTypeName(eventDeclaration));
    }

    private void mapEventSubscription(MethodInvocation node, final String eventArgsType, final String eventHandlerType) {
        final CSAnonymousClassBuilder listenerBuilder = mapAnonymousEventListener(node);
        final CSMemberReferenceExpression handlerMethodRef = new CSMemberReferenceExpression(listenerBuilder
                .createConstructorInvocation(), eventListenerMethodName(listenerBuilder));

        final CSReferenceExpression delegateType = new CSReferenceExpression(eventHandlerType);

        patchEventListener(listenerBuilder, eventArgsType);

        CSConstructorInvocationExpression delegateConstruction = new CSConstructorInvocationExpression(delegateType);
        delegateConstruction.addArgument(handlerMethodRef);

        pushExpression(new CSInfixExpression("+=", mapMethodTargetExpression(node), delegateConstruction));
    }

    private CSAnonymousClassBuilder mapAnonymousEventListener(MethodInvocation node) {
        ClassInstanceCreation creation = (ClassInstanceCreation) node.arguments().get(0);
        return mapAnonymousClass(creation.getAnonymousClassDeclaration());
    }

    private String eventListenerMethodName(final CSAnonymousClassBuilder listenerBuilder) {
        return mappedMethodName(getFirstMethod(listenerBuilder.anonymousBaseType()));
    }

    private void patchEventListener(CSAnonymousClassBuilder listenerBuilder, String eventArgsType) {
        final CSClass type = listenerBuilder.type();
        type.clearBaseTypes();

        final CSMethod handlerMethod = (CSMethod) type.getMember(eventListenerMethodName(listenerBuilder));
        handlerMethod.parameters().get(0).type(OBJECT_TYPE_REFERENCE);
        handlerMethod.parameters().get(0).name("sender");
        handlerMethod.parameters().get(1).type(new CSTypeReference(eventArgsType));

    }

    private IMethodBinding getFirstMethod(ITypeBinding listenerType) {
        return listenerType.getDeclaredMethods()[0];
    }

    private void assertValidEventAddListener(ASTNode source, MethodDeclaration addListener) {
        if (isValidEventAddListener(addListener))
            return;

        unsupportedConstruct(source, SharpenAnnotations.SHARPEN_EVENT_ADD + " must take lone single method interface argument");
    }

    private boolean isValidEventAddListener(MethodDeclaration addListener) {
        if (1 != addListener.parameters().size())
            return false;

        final ITypeBinding type = getFirstParameterType(addListener);
        if (!type.isInterface())
            return false;

        return type.getDeclaredMethods().length == 1;
    }

    private ITypeBinding getFirstParameterType(MethodDeclaration addListener) {
        return parameter(addListener, 0).getType().resolveBinding();
    }

    private SingleVariableDeclaration parameter(MethodDeclaration method, final int index) {
        return (SingleVariableDeclaration) method.parameters().get(index);
    }

    private boolean isEventSubscription(MethodInvocation node) {
        return isTaggedMethodInvocation(node, SharpenAnnotations.SHARPEN_EVENT_ADD);
    }

    private boolean isMappedEventSubscription(MethodInvocation node) {
        return _configuration.isMappedEventAdd(qualifiedName(node));
    }

    private String qualifiedName(MethodInvocation node) {
        return qualifiedName(node.resolveMethodBinding());
    }

    private boolean isTaggedMethodInvocation(MethodInvocation node, final String tag) {
        return isTaggedMethodInvocation(node.resolveMethodBinding(), tag);
    }

    private boolean isTaggedMethodInvocation(final IMethodBinding binding, final String tag) {
        final BodyDeclaration method = declaringNode(originalMethodBinding(binding));
        if (null == method) {
            return false;
        }
        return containsJavadoc(method, tag);
    }

    @SuppressWarnings("unchecked")
    private void processMappedMethodInvocation(MethodInvocation node, IMethodBinding binding,
                                               Configuration.MemberMapping mapping) {

        if (mapping.kind == MemberKind.Indexer) {
            processIndexerInvocation(node);
            return;
        }
        if (mapping.kind == MemberKind.IndexerGetOr) {
            processIndexerGetOr(node);
            return;
        }

        String name = mappedMethodName(binding);
        if (0 == name.length()) {
            final Expression expression = node.getExpression();
            final CSExpression target = expression != null ? mapExpression(expression) : new CSThisExpression(); // see
            // collections/EntrySet1
            pushExpression(target);
            return;
        }

        boolean isMappingToStaticMethod = isMappingToStaticMember(name);

        List<Expression> arguments = node.arguments();
        CSExpression expression = isMappingToStaticMethod ? null : mapMethodTargetExpression(node);
        CSExpression target;

        if (null == expression) {
            target = new CSReferenceExpression(name);
        } else {
            if (BindingUtils.isStatic(binding) && arguments.size() > 0) {
                // mapping static method to instance member
                // typical example is String.valueOf(arg) => arg.ToString()
                target = new CSMemberReferenceExpression(parensIfNeeded(mapExpression(arguments.get(0))), name);
                arguments = arguments.subList(1, arguments.size());
            } else {
                target = new CSMemberReferenceExpression(expression, name);
            }
        }

        if (mapping.kind != MemberKind.Method) {
            IMethodBinding originalBinding = node.resolveMethodBinding();
            if (binding != originalBinding && originalBinding.getReturnType() != binding.getReturnType() && !(node.getParent() instanceof ExpressionStatement)) {
                target = new CSParenthesizedExpression(new CSCastExpression(mappedTypeReference(originalBinding.getReturnType()), target));
            }
            switch (arguments.size()) {
                case 0:
                    pushExpression(target);
                    break;

                case 1:
                    pushExpression(new CSInfixExpression("=", target, mapExpression(arguments.get(0))));
                    break;

                default:
                    unsupportedConstruct(node, "Method invocation with more than 1 argument mapped to property");
                    break;
            }
            return;
        }

        CSMethodInvocationExpression mie = new CSMethodInvocationExpression(target);
        if (isMappingToStaticMethod && isInstanceMethod(binding)) {
            expression = mapMethodTargetExpression(node);
            if (null == expression) {
                mie.addArgument(new CSThisExpression());
            } else {
                mie.addArgument(expression);
            }
        }
        mapArguments(mie, arguments);
        adjustJUnitArguments(mie, node);
        pushExpression(mie);
    }

    private void processIndexerInvocation(MethodInvocation node) {
        if (node.arguments().size() == 1) {
            processIndexerGetter(node);
        } else {
            processIndexerSetter(node);
        }
    }

    private void processIndexerSetter(MethodInvocation node) {
        // target(arg0 ... argN) => target[arg0... argN-1] = argN;

        final CSIndexedExpression indexer = new CSIndexedExpression(mapIndexerTarget(node));
        final List arguments = node.arguments();
        final Expression lastArgument = (Expression) arguments.get(arguments.size() - 1);
        for (int i = 0; i < arguments.size() - 1; ++i) {
            indexer.addIndex(mapExpression((Expression) arguments.get(i)));
        }
        pushExpression(CSharpCode.newAssignment(indexer, mapExpression(lastArgument)));

    }

    private void processIndexerGetter(MethodInvocation node) {
        final Expression singleArgument = (Expression) node.arguments().get(0);
        pushExpression(new CSIndexedExpression(mapIndexerTarget(node), mapExpression(singleArgument)));
    }

    private void processIndexerGetOr(MethodInvocation node) {
        ITypeBinding typeBinding = node.resolveTypeBinding();
        if (typeBinding.isTypeVariable() || BindingUtils.isWildcard(typeBinding)) {
            processIndexerGetter(node);
            return;
        }
        if (BindingUtils.isPrimitiveBox(typeBinding)) {
            final Expression singleArgument = (Expression) node.arguments().get(0);
            pushExpression(new CSMethodInvocationExpression(new CSMemberReferenceExpression(mapIndexerTarget(node), "GetOrNullable"), mapExpression(singleArgument)));
        } else {
            final Expression singleArgument = (Expression) node.arguments().get(0);
            pushExpression(new CSMethodInvocationExpression(new CSMemberReferenceExpression(mapIndexerTarget(node), "GetOrNull"), mapExpression(singleArgument)));
        }
    }

    private CSExpression mapIndexerTarget(MethodInvocation node) {
        if (node.getExpression() == null) {
            return new CSThisExpression();
        }
        return mapMethodTargetExpression(node);
    }

    private CSExpression parensIfNeeded(CSExpression expression) {
        if (expression instanceof CSInfixExpression || expression instanceof CSPrefixExpression
                || expression instanceof CSPostfixExpression) {

            return new CSParenthesizedExpression(expression);
        }
        return expression;
    }

    protected CSExpression mapMethodTargetExpression(MethodInvocation node) {
        return mapExpression(node.getExpression());
    }

    private boolean isInstanceMethod(IMethodBinding binding) {
        return !BindingUtils.isStatic(binding);
    }

    private boolean isMappingToStaticMember(String name) {
        return -1 != name.indexOf('.');
    }

    protected void mapArguments(CSMethodInvocationExpression mie, List arguments) {
        for (Object arg : arguments) {
            addArgument(mie, (Expression) arg, null);
        }
    }

    private void addArgument(CSMethodInvocationExpression mie, Expression arg, ITypeBinding expectedType) {
        mie.addArgument(mapExpression(expectedType, arg));
    }

    public boolean visit(FieldAccess node) {
        String name = mappedFieldName(node);
        name = _resolver.resolveRename(node.resolveFieldBinding(), name);
        if (null == node.getExpression()) {
            pushExpression(new CSReferenceExpression(name));
        } else {
            pushExpression(new CSMemberReferenceExpression(mapExpression(node.getExpression()), name));
        }
        return false;
    }

    private String mapVariableName(String name) {
        if (_renamedVariables.size() > 0) {
            String vname = name;
            if (vname.startsWith("@"))
                vname = vname.substring(1);
            String newName = _renamedVariables.peek().get(vname);
            if (newName != null)
                return newName;
        }
        return name;
    }

    private boolean isBoolLiteral(String name) {
        return name.equals("true") || name.equals("false");
    }

    private String mappedFieldName(FieldAccess node) {
        String name = mappedFieldName(node.getName());
        if (null != name)
            return name;
        return identifier(node.getName());
    }

    public boolean visit(SimpleName node) {
        if (isTypeReference(node)) {
            pushTypeReference(node.resolveTypeBinding());
        } else if (_currentExpression == null) {
            String ident = mapVariableName(identifier(node));
            IBinding b = node.resolveBinding();
            ident = _resolver.resolveRename(b, ident);
            IVariableBinding vb = b instanceof IVariableBinding ? (IVariableBinding) b : null;
            if (vb != null) {
                if (nullablePrimitivesVariables.contains(b.getKey()) && shouldUnboxNullablePrimitive(node)) {
                    pushExpression(new CSMemberReferenceExpression(new CSReferenceExpression(ident), "Value"));
                    return false;
                }
                ITypeBinding vbType = vb.getType();
                if ("java.lang.Class".equals(qualifiedName(vbType)) && vbType.getTypeArguments().length > 0 && !vbType.getTypeArguments()[0].getName().equals("?")) {
                    pushExpression(new CSTypeofExpression(genericRuntimeTypeIdiomType(vbType)));
                    return false;
                }
                ITypeBinding cls = vb.getDeclaringClass();
                if (cls != null) {
                    if (isStaticImport(vb, _ast.imports())) {
                        pushExpression(new CSMemberReferenceExpression(mappedTypeReference(cls), ident));
                        return false;
                    } else if (_configuration.separateInterfaceConstants() && cls.isInterface() && ident.indexOf('.') == -1) {
                        pushExpression(new CSMemberReferenceExpression(mappedAuxillaryTypeReference(cls), ident));
                        return false;
                    }
                }
            }
            pushExpression(new CSReferenceExpression(ident));
        }
        return false;
    }

    private boolean shouldUnboxNullablePrimitive(SimpleName node) {
        ASTNode parent = node.getParent();

        if (parent instanceof InfixExpression) {
            InfixExpression infix = (InfixExpression) parent;
            if (isNullCheck(infix)) {
                return false;
            }
            if (isStringConcat(infix)) {
                return false;
            }
        }
        if (parent instanceof MethodInvocation) {
            MethodInvocation methodInvocation = (MethodInvocation) parent;
            int argumentIndex = findMethodInvocationArgumentIndex(methodInvocation, node);
            IMethodBinding b = methodInvocation.resolveMethodBinding();
            ITypeBinding argumentType = b.getParameterTypes()[argumentIndex];
            if (!BindingUtils.isPrimitive(argumentType) && !BindingUtils.isPrimitiveBox(argumentType)) {
                return false;
            }
        }
        return true;
    }

    private boolean isNullCheck(InfixExpression infix) {
        return (infix.getOperator() == InfixExpression.Operator.EQUALS || infix.getOperator() == InfixExpression.Operator.NOT_EQUALS) &&
                (infix.getLeftOperand() instanceof NullLiteral || infix.getRightOperand() instanceof NullLiteral);
    }

    private boolean isStringConcat(InfixExpression infix) {
        if (infix.getOperator() != InfixExpression.Operator.PLUS) {
            return false;
        }
        return "java.lang.String".equals(infix.getLeftOperand().resolveTypeBinding().getQualifiedName()) ||
                "java.lang.String".equals(infix.getRightOperand().resolveTypeBinding().getQualifiedName());
    }

    private int findMethodInvocationArgumentIndex(MethodInvocation invocation, SimpleName node) {
        List arguments = invocation.arguments();
        for (int i = 0; i < arguments.size(); i++) {
            Object o = arguments.get(i);
            if (o instanceof SimpleName && o == node) {
                return i;
            }
        }
        throw new RuntimeException("Could not find argument " + node + " in " + invocation);
    }

    private void addStatement(CSStatement statement) {
        _currentBlock.addStatement(statement);
    }

    private void pushTypeReference(ITypeBinding typeBinding) {
        pushExpression(mappedTypeReference(typeBinding));
    }

    protected CSReferenceExpression createTypeReference(ITypeBinding typeBinding) {
        return new CSReferenceExpression(mappedTypeName(typeBinding));
    }

    private boolean isTypeReference(Name node) {
        final IBinding binding = node.resolveBinding();
        if (null == binding) {
            unresolvedTypeBinding(node);
            return false;
        }
        return IBinding.TYPE == binding.getKind();
    }

    public boolean visit(QualifiedName node) {
        if (isTypeReference(node)) {
            pushTypeReference(node.resolveTypeBinding());
        } else {
            String primitiveTypeRef = checkForPrimitiveTypeReference(node);
            if (primitiveTypeRef != null) {
                pushTypeOfExpression(new CSTypeReference(primitiveTypeRef));
            } else {
                handleRegularQualifiedName(node);
            }
        }
        return false;
    }

    private void handleRegularQualifiedName(QualifiedName node) {
        String mapped = mappedFieldName(node);
        if (null != mapped) {
            if (isBoolLiteral(mapped)) {
                pushExpression(new CSBoolLiteralExpression(Boolean.parseBoolean(mapped)));
                return;
            }
            if (isMappingToStaticMember(mapped)) {
                pushExpression(new CSReferenceExpression(mapped));
            } else {
                pushMemberReferenceExpression(node.getQualifier(), mapped);
            }
        } else {
            Name qualifier = node.getQualifier();
            String name = identifier(node.getName().getIdentifier());
            pushMemberReferenceExpression(qualifier, name);
        }
    }

    private String checkForPrimitiveTypeReference(QualifiedName node) {
        String name = qualifiedName(node);
        if (name.equals(JAVA_LANG_VOID_TYPE))
            return "void";
        if (name.equals(JAVA_LANG_BOOLEAN_TYPE))
            return "bool";
        if (name.equals(JAVA_LANG_BYTE_TYPE)) {
            return _configuration.mapByteToSbyte() ?
                    "sbyte" :
                    "byte";
        }
        if (name.equals(JAVA_LANG_CHARACTER_TYPE))
            return "char";
        if (name.equals(JAVA_LANG_SHORT_TYPE))
            return "short";
        if (name.equals(JAVA_LANG_INTEGER_TYPE))
            return "int";
        if (name.equals(JAVA_LANG_LONG_TYPE))
            return "long";
        if (name.equals(JAVA_LANG_FLOAT_TYPE))
            return "float";
        if (name.equals(JAVA_LANG_DOUBLE_TYPE))
            return "double";
        return null;
    }

    private String qualifiedName(QualifiedName node) {
        IVariableBinding binding = variableBinding(node);
        if (binding == null)
            return node.toString();
        return BindingUtils.qualifiedName(binding);
    }

    private void pushMemberReferenceExpression(Name qualifier, String name) {
        if (_configuration.separateInterfaceConstants()) {
            IBinding binding = qualifier.resolveBinding();
            if (binding instanceof ITypeBinding) {
                ITypeBinding typeBinding = (ITypeBinding) binding;
                if (typeBinding.isInterface()) {
                    pushExpression(new CSMemberReferenceExpression(mappedAuxillaryTypeReference(typeBinding), name));
                    return;
                }
            }
        }

        pushExpression(new CSMemberReferenceExpression(mapExpression(qualifier), name));
    }

    private IVariableBinding variableBinding(Name node) {
        if (node.resolveBinding() instanceof IVariableBinding) {
            return (IVariableBinding) node.resolveBinding();
        }
        return null;
    }

    private String mappedFieldName(SuperFieldAccess node) {
        if (node.getQualifier() != null) {
            notImplemented(node);
        }

        String name = mappedFieldName(node.getName());
        if (null != name)
            return name;

        return identifier(node.getName());
    }

    private String mappedFieldName(Name node) {
        IVariableBinding binding = variableBinding(node);
        return null == binding ? null : my(Mappings.class).mappedFieldName(binding);
    }

    protected CSExpression mapExpression(Expression expression) {
        if (null == expression)
            return null;

        try {
            expression.accept(this);
            return popExpression();
        } catch (Exception e) {
            unsupportedConstruct(expression, e);
            return null; // we'll never get here
        }
    }

    private void unsupportedConstruct(ASTNode node, Exception cause) {
        throw new IllegalArgumentException(sourceInformation(node) + ": " + ("failed to map: '" + node + "'"), cause);
    }

    private void unsupportedConstruct(ASTNode node, String message) {
        throw new IllegalArgumentException(sourceInformation(node) + ": " + message);
    }

    private ITypeBinding pushExpectedType(ITypeBinding type) {
        ITypeBinding old = _currentExpectedType;
        _currentExpectedType = type;
        return old;
    }

    private void popExpectedType(ITypeBinding saved) {
        _currentExpectedType = saved;
    }

    protected void pushExpression(CSExpression expression) {
        if (null != _currentExpression) {
            throw new IllegalStateException();
        }
        _currentExpression = expression;
    }

    private CSExpression popExpression() {
        if (null == _currentExpression) {
            throw new IllegalStateException();
        }
        CSExpression found = _currentExpression;
        _currentExpression = null;
        return found;
    }

    private CSVariableDeclaration createParameter(SingleVariableDeclaration declaration) {
        return createVariableDeclaration(declaration.resolveBinding(), null);
    }

    protected void visit(List nodes) {
        for (Object node : nodes) {
            ((ASTNode) node).accept(this);
        }
    }

    private void createInheritedAbstractMemberStubs(AbstractTypeDeclaration node) {
        if (node instanceof TypeDeclaration && ((TypeDeclaration) node).isInterface())
            return;

        ITypeBinding binding = node.resolveBinding();
        if (!Modifier.isAbstract(node.getModifiers()))
            return;

        Set<ITypeBinding> interfaces = new LinkedHashSet<ITypeBinding>();
        collectInterfaces(interfaces, binding);
        for (ITypeBinding baseType : interfaces) {
            createInheritedAbstractMemberStubs(binding, baseType);
        }
    }

    private void collectInterfaces(Set<ITypeBinding> interfaceList, ITypeBinding binding) {
        ITypeBinding[] interfaces = binding.getInterfaces();
        for (int i = 0; i < interfaces.length; ++i) {
            ITypeBinding interfaceBinding = interfaces[i];
            if (interfaceList.contains(interfaceBinding)) {
                continue;
            }
            collectInterfaces(interfaceList, interfaceBinding);
            interfaceList.add(interfaceBinding);
        }
    }

    private void createInheritedAbstractMemberStubs(ITypeBinding type, ITypeBinding baseType) {
        IMethodBinding[] methods = baseType.getDeclaredMethods();
        for (int i = 0; i < methods.length; ++i) {
            IMethodBinding method = methods[i];
            if (!Modifier.isAbstract(method.getModifiers())) {
                continue;
            }
            if (null != BindingUtils.findOverriddenMethodInTypeOrSuperclasses(type, method)) {
                continue;
            }
            if (isIgnored(originalMethodBinding(method))) {
                continue;
            }
            if (stubIsProperty(method)) {
                _currentType.addMember(createAbstractPropertyStub(method));
            } else {
                CSMethod newMethod = createAbstractMethodStub(method);
                //the same method might be defined in multiple interfaces
                //but only a single stub must be created for those
                if (!_currentType.members().contains(newMethod)) {
                    _currentType.addMember(newMethod);
                }
            }
        }
    }

    private boolean isIgnored(IMethodBinding binding) {
        final BodyDeclaration dec = declaringNode(binding);
        return dec != null && SharpenAnnotations.hasIgnoreAnnotation(dec);
    }

    private boolean stubIsProperty(IMethodBinding method) {
        final BodyDeclaration dec = declaringNode(method);
        return dec != null && isProperty(dec);
    }

    private BodyDeclaration declaringNode(IMethodBinding method) {
        return findDeclaringNode(method);
    }

    private CSProperty createAbstractPropertyStub(IMethodBinding method) {
        CSProperty stub = newAbstractPropertyStubFor(method);
        safeProcessDisableTags(method, stub);

        return stub;
    }

    private CSProperty newAbstractPropertyStubFor(IMethodBinding method) {
        CSProperty stub = new CSProperty(mappedMethodName(method), mappedTypeReference(method.getReturnType()));
        stub.modifier(CSMethodModifier.Abstract);
        stub.visibility(mapVisibility(method.getModifiers()));
        stub.getter(new CSBlock());
        return stub;
    }

    private CSMethod createAbstractMethodStub(IMethodBinding method) {
        CSMethod stub = newAbstractMethodStubFor(method);
        safeProcessDisableTags(method, stub);

        return stub;
    }

    private CSMethod newAbstractMethodStubFor(IMethodBinding method) {
        CSMethod stub = new CSMethod(mappedMethodName(method));

        stub.modifier(CSMethodModifier.Abstract);
        stub.visibility(mapVisibility(method.getModifiers()));
        stub.returnType(mappedTypeReference(method.getReturnType()));

        ITypeBinding[] parameters = method.getParameterTypes();
        for (int i = 0; i < parameters.length; ++i) {
            stub.addParameter(new CSVariableDeclaration("arg" + (i + 1), mappedTypeReference(parameters[i])));
        }
        return stub;
    }

    private void safeProcessDisableTags(IMethodBinding method, CSMember member) {
        final BodyDeclaration node = declaringNode(method);
        if (node == null) return;

        processDisableTags(node, member);
    }

    CSMethodModifier mapMethodModifier(MethodDeclaration method) {
        if (_currentType.isInterface() || method.resolveBinding().getDeclaringClass().isInterface()) {
            return CSMethodModifier.Abstract;
        }
        int modifiers = method.getModifiers();
        if (Modifier.isStatic(modifiers)) {
            return CSMethodModifier.Static;
        }
        if (Modifier.isPrivate(modifiers)) {
            return CSMethodModifier.None;
        }

        boolean override = isOverride(method);
        if (Modifier.isAbstract(modifiers)) {
            return override ? CSMethodModifier.AbstractOverride : CSMethodModifier.Abstract;
        }
        boolean isFinal = Modifier.isFinal(modifiers);
        if (override) {
            return isFinal ? CSMethodModifier.Sealed : modifierIfNewAnnotationNotApplied(method, CSMethodModifier.Override);
        }
        return isFinal || _currentType.isSealed() ? CSMethodModifier.None : CSMethodModifier.Virtual;
    }

    private CSMethodModifier modifierIfNewAnnotationNotApplied(MethodDeclaration method, CSMethodModifier modifier) {
        return containsJavadoc(method, SharpenAnnotations.SHARPEN_NEW)
                ? CSMethodModifier.None
                : modifier;
    }

    private boolean isExtractedNestedType(ITypeBinding type) {
        return _configuration.typeHasMapping(BindingUtils.typeMappingKey(type));
    }

    private boolean isOverride(MethodDeclaration method) {
        return null != getOverridedMethod(method);
    }

    private IMethodBinding getOverridedMethod(MethodDeclaration method) {
        return getOverridedMethod(method.resolveBinding());
    }

    private IMethodBinding getOverridedMethod(IMethodBinding methodBinding) {
        ITypeBinding superclass = _ignoreExtends.value() ? resolveWellKnownType("java.lang.Object") : methodBinding
                .getDeclaringClass().getSuperclass();
        if (null != superclass) {
            IMethodBinding result = BindingUtils.findOverriddenMethodInHierarchy(superclass, methodBinding);
            if (null != result)
                return result;
        }

        if (!_configuration.separateInterfaceConstants()) {
            ITypeBinding[] baseInterfaces = methodBinding.getDeclaringClass().getInterfaces();
            if (baseInterfaces.length == 1 && !isValidCSInterface(baseInterfaces[0])) {
                // Base interface generated as a class
                return BindingUtils.findOverriddenMethodInType(baseInterfaces[0], methodBinding);
            }
        }

        return null;
    }

    private boolean isValidCSInterface(ITypeBinding type) {
        if (type.getTypeDeclaration().getQualifiedName().equals("java.util.Iterator") || type.getTypeDeclaration().getQualifiedName().equals("java.lang.Iterable"))
            return false;
        if (type.getDeclaredFields().length != 0)
            return false;
        for (ITypeBinding ntype : type.getDeclaredTypes()) {
            if (!isExtractedNestedType(ntype))
                return false;
        }
        return true;
    }

    CSClassModifier mapClassModifier(int modifiers) {
        if (Modifier.isAbstract(modifiers)) {
            return CSClassModifier.Abstract;
        }
        if (Modifier.isFinal(modifiers)) {
            return CSClassModifier.Sealed;
        }
        return CSClassModifier.None;
    }

    void adjustVisibility(ITypeBinding memberType, CSMember member) {
        if (memberType == null)
            return;
        CSVisibility typeVisibility = mapVisibility(memberType.getModifiers());
        if (typeVisibility == CSVisibility.Protected && member.visibility() == CSVisibility.Internal)
            member.visibility(CSVisibility.Protected);
    }

    CSVisibility mapVisibility(BodyDeclaration node) {
        if (containsJavadoc(node, SharpenAnnotations.SHARPEN_INTERNAL)) {
            return CSVisibility.Internal;
        }

        if (containsJavadoc(node, SharpenAnnotations.SHARPEN_PRIVATE)) {
            return CSVisibility.Private;
        }

        if (containsJavadoc(node, SharpenAnnotations.SHARPEN_PROTECTED)) {
            return CSVisibility.Protected;
        }

        if (containsJavadoc(node, SharpenAnnotations.SHARPEN_PUBLIC)) {
            return CSVisibility.Public;
        }

        CSVisibility defaultVisibility = CSVisibility.Internal;
        if (node.getParent() instanceof TypeDeclaration && ((TypeDeclaration) node.getParent()).isInterface()) {
            defaultVisibility = CSVisibility.Public;
        } else if (node instanceof EnumConstantDeclaration) {
            defaultVisibility = CSVisibility.Public;
        }

        return mapVisibility(node.getModifiers(), defaultVisibility);
    }

    CSVisibility mapVisibility(int modifiers) {
        return mapVisibility(modifiers, CSVisibility.Internal);
    }

    CSVisibility mapVisibility(int modifiers, CSVisibility defaultVisibility) {
        if (Modifier.isPublic(modifiers)) {
            return CSVisibility.Public;
        }
        if (Modifier.isProtected(modifiers)) {
            return _configuration.mapProtectedToProtectedInternal() ?
                    CSVisibility.ProtectedInternal :
                    CSVisibility.Protected;
        }
        if (Modifier.isPrivate(modifiers)) {
            return CSVisibility.Private;
        }
        return defaultVisibility;
    }

    protected CSTypeReferenceExpression mappedTypeReference(Type type, boolean forTypeOf) {
        return mappedTypeReference(type.resolveBinding(), false, forTypeOf);
    }

    protected CSTypeReferenceExpression mappedArrayReference(Type type, int dimension) {
        ITypeBinding binding = type.resolveBinding();
        if (dimension > 0) {
            binding = binding.createArrayType(dimension);
        }
        return mappedTypeReference(binding);
    }

    private CSTypeReferenceExpression mappedMacroTypeReference(ITypeBinding typeUsage, final TypeDeclaration typeDeclaration) {

        final CSMacro macro = new CSMacro(JavadocUtility.singleTextFragmentFrom(javadocTagFor(typeDeclaration, SharpenAnnotations.SHARPEN_MACRO)));

        final ITypeBinding[] typeArguments = typeUsage.getTypeArguments();
        if (typeArguments.length > 0) {
            final ITypeBinding[] typeParameters = typeUsage.getTypeDeclaration().getTypeParameters();
            for (int i = 0; i < typeParameters.length; i++) {
                macro.addVariable(typeParameters[i].getName(), mappedTypeReference(typeArguments[i]));
            }
        }

        return new CSMacroTypeReference(macro);
    }

    private boolean isMacroType(final ASTNode declaration) {
        return declaration instanceof TypeDeclaration
                && containsJavadoc((TypeDeclaration) declaration, SharpenAnnotations.SHARPEN_MACRO);
    }

    protected CSTypeReferenceExpression mappedTypeReference(ITypeBinding type) {
        return mappedTypeReference(type, false, false);
    }

    protected CSTypeReferenceExpression mappedAuxillaryTypeReference(ITypeBinding type) {
        return mappedTypeReference(type, true, false);
    }

    protected CSTypeReferenceExpression mappedTypeReference(ITypeBinding type, boolean auxillary, boolean forTypeOf) {
        if (auxillary && !type.isInterface()) {
            throw new IllegalArgumentException("Auxillary types are only available for interfaces.");
        }

        final ASTNode declaration = findDeclaringNode(type);
        if (isMacroType(declaration)) {
            return mappedMacroTypeReference(type, (TypeDeclaration) declaration);
        }

        if (type.isArray()) {
            return mappedArrayTypeReference(type);
        }
        if (type.isWildcardType()) {
            return mappedWildcardTypeReference(type);
        }

        if (type.isInterface() && auxillary) {
            return new CSTypeReference(type.getName() + "Constants");
        }

        CSTypeReference typeRef = CSTypeReference.parseGeneric(mappedTypeName(type));
        if (BindingUtils.isJavaLangClass(type)) {
            return typeRef;
        }

        for (ITypeBinding arg : type.getTypeArguments()) {
            typeRef.addTypeArgument(mappedTypeReference(arg));
        }

        // to convert expressions like Map.class to typeof(Map<,>)
        if (forTypeOf && type.getTypeArguments().length == 0 && type.getTypeDeclaration().getTypeParameters().length > 0
                && !_configuration.isTypeMappedToNonGenericCollection(type.getQualifiedName())
                && !type.getQualifiedName().equals("java.lang.Enum")) {
            typeRef = new CSTypeReference(typeRef.typeName());
            for (Object ignored : type.getTypeDeclaration().getTypeParameters()) {
                typeRef.addTypeArgument(new CSTypeReference(""));
            }
        }

        return typeRef;
    }

    private CSTypeReferenceExpression mappedWildcardTypeReference(ITypeBinding type) {
        final ITypeBinding bound = type.getBound();
        return bound != null ? mappedTypeReference(bound) : OBJECT_TYPE_REFERENCE;
    }

    private CSTypeReferenceExpression mappedArrayTypeReference(ITypeBinding type) {
        return new CSArrayTypeReference(mappedTypeReference(type.getElementType()), type.getDimensions());

    }

    protected final String mappedTypeName(ITypeBinding type) {
        return my(Mappings.class).mappedTypeName(type);
    }

    private static String qualifiedName(ITypeBinding type) {
        return BindingUtils.qualifiedName(type);
    }

    private String interfaceName(String name) {
        return my(Configuration.class).toInterfaceName(name);
    }

    private String mappedTypeName(String typeName) {
        return mappedTypeName(typeName, typeName);
    }

    private String mappedTypeName(String typeName, String defaultValue) {
        return _configuration.mappedTypeName(typeName, defaultValue);
    }

    private String annotatedRenaming(BodyDeclaration node) {
        return my(Annotations.class).annotatedRenaming(node);
    }

    protected String mappedMethodName(MethodDeclaration node) {
        return mappedMethodName(node.resolveBinding());
    }

    protected final String mappedMethodName(IMethodBinding binding) {
        return my(Mappings.class).mappedMethodName(binding);
    }

    private String qualifiedName(IMethodBinding actual) {
        return BindingUtils.qualifiedName(actual);
    }

    private boolean isEvent(MethodDeclaration declaring) {
        return eventTagFor(declaring) != null;
    }

    private boolean isMappedToProperty(BodyDeclaration original) {
        IMethodBinding binding;
        if (original instanceof MethodDeclaration) {
            binding = ((MethodDeclaration) original).resolveBinding();
        } else if (original instanceof AnnotationTypeMemberDeclaration) {
            binding = ((AnnotationTypeMemberDeclaration) original).resolveBinding();
        } else {
            throw new UnsupportedOperationException();
        }

        final MemberMapping mapping = effectiveMappingFor(binding);
        if (null == mapping)
            return false;
        return mapping.kind == MemberKind.Property;
    }

    private MemberMapping effectiveMappingFor(IMethodBinding binding) {
        return my(Mappings.class).effectiveMappingFor(binding);
    }

    private String methodName(String name) {
        return namingStrategy().methodName(name);
    }

    protected String identifier(SimpleName name) {
        return identifier(name.toString());
    }

    protected String identifier(String name) {
        return namingStrategy().identifier(name);
    }

    private void unresolvedTypeBinding(ASTNode node) {
        warning(node, "unresolved type binding for node: " + node);
    }

    public boolean visit(CompilationUnit node) {
        return true;
    }

    private void warning(ASTNode node, String message) {
        warningHandler().warning(node, message);
    }

    protected final String sourceInformation(ASTNode node) {
        return ASTUtility.sourceInformation(_ast, node);
    }

    @SuppressWarnings("deprecation")
    protected int lineNumber(ASTNode node) {
        return _ast.lineNumber(node.getStartPosition());
    }

    private String mappedNamespace(String namespace) {
        return _configuration.mappedNamespace(namespace);
    }

    @Override
    public boolean visit(Block node) {
        if (isBlockInsideBlock(node)) {
            CSBlock parent = _currentBlock;
            _currentBlock = new CSBlock();
            _currentBlock.parent(parent);
            parent.addStatement(_currentBlock);
        }
        _currentContinueLabel = null;
        pushScope();
        return super.visit(node);
    }

    @Override
    public void endVisit(Block node) {
        if (isBlockInsideBlock(node)) {
            _currentBlock = (CSBlock) _currentBlock.parent();
        }
        popScope();
        super.endVisit(node);
    }

    @Override
    public boolean visit(LambdaExpression node) {
        List<CSVariableDeclaration> args = new ArrayList<>();
        for (Object param : node.parameters()) {
            IVariableBinding variableBinding = ((VariableDeclarationFragment) param).resolveBinding();
            args.add(new CSVariableDeclaration(variableBinding.getName(), mappedTypeReference(variableBinding.getType())));
        }
        CSNode body;
        if (node.getBody() instanceof Expression) {
            body = mapExpression((Expression) node.getBody());
        } else if (node.getBody() instanceof Block) {
            CSBlock block = new CSBlock();
            visitBlock(block, node.getBody());
            body = block;
        } else {
            notImplemented(node);
            return false;
        }
        pushExpression(new CSLambdaExpression(args, body));
        return false;
    }

    boolean isBlockInsideBlock(Block node) {
        return node.getParent() instanceof Block;
    }

    void pushScope() {
        HashSet<String> newLocalVars = new HashSet<String>();
        if (_localBlockVariables.size() > 0)
            newLocalVars.addAll(_localBlockVariables.peek());
        _localBlockVariables.push(newLocalVars);

        HashSet<String> newBlockVars = new HashSet<String>();
        newBlockVars.addAll(newLocalVars);
        _blockVariables.push(newBlockVars);

        HashMap<String, String> newRenamed = new HashMap<String, String>();
        if (_renamedVariables.size() > 0)
            newRenamed.putAll(_renamedVariables.peek());
        _renamedVariables.push(newRenamed);
    }

    void popScope() {
        _blockVariables.pop();
        _localBlockVariables.pop();
        _renamedVariables.pop();
    }
}
